diff --git a/assets/feature_flags.json b/assets/feature_flags.json new file mode 100644 index 0000000..61fdec6 --- /dev/null +++ b/assets/feature_flags.json @@ -0,0 +1,12 @@ +{ + "features": { + "articles.credit_section": { + "enabled": false, + "description": "Abschnitt „Gutschriften“ (Betrags-Gutschrift) im Artikel-Step der Lieferung. Aus = komplett ausgeblendet." + }, + "articles.credit_amount_stepper": { + "enabled": false, + "description": "Eingabe der Betrags-Gutschrift per +/−-Stepper in 10-€-Schritten statt freiem Textfeld." + } + } +} diff --git a/lib/bloc/app_bloc.dart b/lib/bloc/app_bloc.dart index d8dc383..f6f5d85 100644 --- a/lib/bloc/app_bloc.dart +++ b/lib/bloc/app_bloc.dart @@ -5,6 +5,7 @@ import 'package:hl_lieferservice/bloc/app_states.dart'; import 'package:hl_lieferservice/data/network/backend_config.dart'; import 'package:hl_lieferservice/data/network/backend_environment.dart'; import 'package:hl_lieferservice/data/network/environment_repository.dart'; +import 'package:hl_lieferservice/feature/feature_flags/feature_flags_repository.dart'; /// Aktiviert das Networking für eine Umgebung (Registrierung im Locator). typedef EnvironmentActivator = void Function(BackendConfig config); @@ -13,14 +14,18 @@ typedef EnvironmentActivator = void Function(BackendConfig config); /// /// Liest die Server-Umgebungen aus `assets/hl_server_config.json`, /// bestimmt die aktive Umgebung (gespeicherte Wahl, sonst Default) und -/// registriert dafür das Networking. Ein Umgebungswechsel läuft über -/// denselben Weg; die UI baut ihren Bloc-Baum pro Umgebung neu auf. +/// registriert dafür das Networking. Außerdem lädt er die Feature-Schalter +/// aus `assets/feature_flags.json`. Ein Umgebungswechsel läuft über denselben +/// Weg; die UI baut ihren Bloc-Baum pro Umgebung neu auf. class AppBloc extends Bloc { AppBloc({ required EnvironmentRepository repository, required EnvironmentActivator activate, + FeatureFlagsRepository featureFlagsRepository = + const FeatureFlagsRepository(), }) : _repository = repository, _activate = activate, + _featureFlagsRepository = featureFlagsRepository, super(const AppInitial()) { on(_onLoad); on(_onSwitch); @@ -28,20 +33,26 @@ class AppBloc extends Bloc { final EnvironmentRepository _repository; final EnvironmentActivator _activate; + final FeatureFlagsRepository _featureFlagsRepository; Future _onLoad(AppLoadConfig event, Emitter emit) async { emit(const AppConfigLoading()); try { final catalog = await _repository.loadCatalog('assets/${event.path}'); + final featureFlags = await _featureFlagsRepository.load(); final storedId = await _repository.loadSelectedId(); // Unbekannte ID (Umgebung aus der Config entfernt) → Default. final active = catalog.byId(storedId) ?? catalog.defaultEnvironment; _activate(catalog.configFor(active)); - emit(AppConfigLoaded(catalog: catalog, active: active)); + emit(AppConfigLoaded( + catalog: catalog, + active: active, + featureFlags: featureFlags, + )); } catch (e, st) { - debugPrint('AppBloc: Server-Config nicht ladbar: $e\n$st'); + debugPrint('AppBloc: Konfiguration nicht ladbar: $e\n$st'); emit(AppConfigLoadingFailed( - message: 'Server-Konfiguration konnte nicht geladen werden.\n$e', + message: 'Konfiguration konnte nicht geladen werden.\n$e', )); } } @@ -59,6 +70,10 @@ class AppBloc extends Bloc { await _repository.saveSelectedId(next.id); _activate(current.catalog.configFor(next)); - emit(AppConfigLoaded(catalog: current.catalog, active: next)); + emit(AppConfigLoaded( + catalog: current.catalog, + active: next, + featureFlags: current.featureFlags, + )); } } diff --git a/lib/bloc/app_states.dart b/lib/bloc/app_states.dart index 6ce2004..fcd6555 100644 --- a/lib/bloc/app_states.dart +++ b/lib/bloc/app_states.dart @@ -1,9 +1,11 @@ import 'package:hl_lieferservice/data/network/backend_environment.dart'; +import 'package:hl_lieferservice/feature/feature_flags/feature_flags.dart'; /// Lifecycle-States des App-Bootstraps. /// /// `AppConfigLoaded` heißt: Server-Config ist gelesen, die aktive Umgebung -/// steht fest und das Networking dafür ist im Locator registriert. +/// steht fest, das Networking dafür ist im Locator registriert und die +/// Feature-Schalter sind geladen. abstract class AppState { const AppState(); } @@ -17,13 +19,20 @@ class AppConfigLoading extends AppState { } class AppConfigLoaded extends AppState { - const AppConfigLoaded({required this.catalog, required this.active}); + const AppConfigLoaded({ + required this.catalog, + required this.active, + required this.featureFlags, + }); /// Alle auswählbaren Umgebungen aus der Config-Datei. final ServerCatalog catalog; /// Die Umgebung, gegen die die App gerade spricht. final BackendEnvironment active; + + /// Feature-Schalter aus `assets/feature_flags.json`. + final FeatureFlags featureFlags; } class AppConfigLoadingFailed extends AppState { diff --git a/lib/domain/entity/delivery.dart b/lib/domain/entity/delivery.dart index d4819d5..8a82181 100644 --- a/lib/domain/entity/delivery.dart +++ b/lib/domain/entity/delivery.dart @@ -11,10 +11,9 @@ enum DeliveryState { active, held, canceled, completed } /// Eine einzelne Auslieferung an einen Kunden innerhalb einer Tour. /// -/// Anders als im alten Modell trägt `Delivery` hier ausschließlich -/// Logistik-Daten — keine Preise, keine Rabatte, keine Zahlungsoptionen. -/// Diese ERP-Themen sind in Phase C+D-2 absichtlich nicht migriert und -/// hängen hinter `FeatureFlags`. +/// Trägt die Logistik-Daten der Lieferung plus die Beträge, die der +/// Zahlungs-Step braucht (Anzahlung, Zahlungsmethode vom Beleg; Stückpreise +/// liegen an den Positionen). class Delivery { const Delivery({ required this.id, diff --git a/lib/feature/delivery/detail/presentation/steps/step_articles.dart b/lib/feature/delivery/detail/presentation/steps/step_articles.dart index 853c886..06208d0 100644 --- a/lib/feature/delivery/detail/presentation/steps/step_articles.dart +++ b/lib/feature/delivery/detail/presentation/steps/step_articles.dart @@ -12,6 +12,8 @@ import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/discount_editor.dart'; import 'package:hl_lieferservice/feature/loading/widget/reason_catalog.dart'; import 'package:hl_lieferservice/feature/loading/widget/reason_picker_sheet.dart'; +import 'package:hl_lieferservice/feature/feature_flags/feature.dart'; +import 'package:hl_lieferservice/feature/feature_flags/feature_gate.dart'; /// Step 3 — Artikel & Gutschriften. /// @@ -99,17 +101,26 @@ class StepArticles extends StatelessWidget { ], ), ), - const SizedBox(height: 24), - _SectionHeader(text: 'Gutschriften'), - const SizedBox(height: 8), - Card( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(16), - child: DiscountEditor( - deliveryId: delivery.id, - active: delivery.state == DeliveryState.active, - ), + // Betrags-Gutschrift, per Feature-Flag komplett ausblendbar. + FeatureGate( + feature: Feature.articlesCreditSection, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + _SectionHeader(text: 'Gutschriften'), + const SizedBox(height: 8), + Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(16), + child: DiscountEditor( + deliveryId: delivery.id, + active: delivery.state == DeliveryState.active, + ), + ), + ), + ], ), ), ], diff --git a/lib/feature/delivery/detail/presentation/widget/discount_editor.dart b/lib/feature/delivery/detail/presentation/widget/discount_editor.dart index 54ccb5f..4767816 100644 --- a/lib/feature/delivery/detail/presentation/widget/discount_editor.dart +++ b/lib/feature/delivery/detail/presentation/widget/discount_editor.dart @@ -3,7 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart'; import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart'; -import 'package:hl_lieferservice/feature/feature_flags.dart'; +import 'package:hl_lieferservice/feature/feature_flags/feature.dart'; +import 'package:hl_lieferservice/feature/feature_flags/feature_flags.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'; @@ -165,8 +166,10 @@ class _DiscountEditorState extends State { ), const SizedBox(height: 8), // Default: freies Betrags-Textfeld. Hinter dem Feature-Flag - // `discountAmountStepper` liegt die ursprüngliche +/−-Variante. - if (FeatureFlags.discountAmountStepper) + // `articles.credit_amount_stepper` liegt die +/−-Variante. + if (context + .read() + .isEnabled(Feature.articlesCreditAmountStepper)) Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/feature/feature_flags.dart b/lib/feature/feature_flags.dart deleted file mode 100644 index db0bf55..0000000 --- a/lib/feature/feature_flags.dart +++ /dev/null @@ -1,46 +0,0 @@ -/// Globale, statische Feature-Schalter. -/// -/// Dient als Übergangs-Geländer während der Migration vom alten -/// ERPframe-Backend auf das neue Rust-Backend: Funktionen, die im neuen -/// Backend (noch) nicht modelliert sind — Rabatte, Zahlungsoptionen, -/// flexible Lieferoptionen, Preisanzeigen, Unterschriften-Upload — -/// werden hier gebündelt ausgeschaltet, statt sie in jedem UI-Widget -/// einzeln auszukommentieren. -/// -/// **Konvention**: jeder Flag bekommt einen kurzen Kommentar, *warum* -/// er gerade auf `false` steht und in welcher Phase der Migration -/// das gegebenenfalls wieder geöffnet wird. So bleibt nachvollziehbar, -/// was hier nur „pausiert" und nicht „weg" ist. -class FeatureFlags { - const FeatureFlags._(); - - /// Rabatt/Gutschrift-Funktion in der Detail-Ansicht. - /// Backend-Modell fehlt — nicht Teil der Logistik-Migration. Wird - /// frühestens nach C+D-2 wiedereröffnet, wenn überhaupt jemals. - static const bool discountsEnabled = false; - - /// Auswahl der Zahlungsart (Bar/EC/Vorkasse) am Ende der Lieferung. - /// Backend modelliert das nicht; die Logistik-App soll bewusst keinen - /// Zahlungs-Workflow tragen. - static const bool paymentsEnabled = false; - - /// Anzeige von Brutto-/Netto-Preisen und Vorauszahlung in der UI. - /// Backend liefert keine Preise — Logistik ≠ Buchhaltung. - static const bool pricesEnabled = false; - - /// Konfigurierbare Lieferoptionen (Treppe, Anschluss, Altgerät, …). - /// Backend-Schema noch nicht vorhanden; geplant für Phase E. - static const bool deliveryOptionsEnabled = false; - - /// Fahrer- und Kunden-Signatur beim Abschluss einer Lieferung. Verkabelt: - /// `SignatureView` → `CompleteDelivery` → multipart `/complete` (Signaturen - /// liegen lokal im Backend-Server). - static const bool signaturesEnabled = true; - - /// Eingabeart der Betrags-Gutschrift im Artikel-Step. - /// `false` → freies Betrags-Textfeld (Default); `true` → der ursprüngliche - /// +/−-Stepper (10-€-Schritte). Hinter dem Flag versteckt, falls der - /// Stepper wieder gewünscht wird. In beiden Fällen gilt die Backend-Regel: - /// >0, ≤150 €, Vielfaches von 10 €. - static const bool discountAmountStepper = false; -} diff --git a/lib/feature/feature_flags/feature.dart b/lib/feature/feature_flags/feature.dart new file mode 100644 index 0000000..d388f79 --- /dev/null +++ b/lib/feature/feature_flags/feature.dart @@ -0,0 +1,20 @@ +/// Alle schaltbaren Features der App. +/// +/// [key] ist der stabile Bezeichner in `assets/feature_flags.json`, über den +/// ein Feature aktiviert oder deaktiviert wird. Der Wert selbst steht nie im +/// Code — nur, was gilt, falls ein Key in der Datei fehlt ([defaultEnabled]). +enum Feature { + /// Abschnitt „Gutschriften" (Betrags-Gutschrift) im Artikel-Step. + articlesCreditSection('articles.credit_section', defaultEnabled: true), + + /// Betrags-Gutschrift per +/−-Stepper statt freiem Textfeld. + articlesCreditAmountStepper( + 'articles.credit_amount_stepper', + defaultEnabled: false, + ); + + const Feature(this.key, {required this.defaultEnabled}); + + final String key; + final bool defaultEnabled; +} diff --git a/lib/feature/feature_flags/feature_flags.dart b/lib/feature/feature_flags/feature_flags.dart new file mode 100644 index 0000000..e125b9c --- /dev/null +++ b/lib/feature/feature_flags/feature_flags.dart @@ -0,0 +1,45 @@ +import 'package:flutter/foundation.dart'; + +import 'feature.dart'; + +/// Unveränderlicher Satz an Feature-Schaltern, geladen aus +/// `assets/feature_flags.json` (siehe [FeatureFlagsRepository]). +/// +/// Erwartetes Format: +/// ```json +/// { "features": { "": { "enabled": true, "description": "…" } } } +/// ``` +/// `description` ist nur Doku für Menschen. Unbekannte Keys werden +/// ignoriert, fehlende fallen auf [Feature.defaultEnabled] zurück. +@immutable +class FeatureFlags { + const FeatureFlags(this._enabledByKey); + + factory FeatureFlags.fromJson(Map json) { + final features = json['features']; + if (features is! Map) { + throw const FormatException('feature_flags.json: "features" fehlt.'); + } + final knownKeys = {for (final f in Feature.values) f.key}; + final enabledByKey = {}; + features.forEach((key, value) { + final enabled = value is Map ? value['enabled'] : null; + if (enabled is! bool) { + throw FormatException( + 'feature_flags.json: "$key.enabled" muss true oder false sein.', + ); + } + if (!knownKeys.contains(key)) { + debugPrint('FeatureFlags: unbekannter Key "$key" wird ignoriert.'); + return; + } + enabledByKey[key] = enabled; + }); + return FeatureFlags(enabledByKey); + } + + final Map _enabledByKey; + + bool isEnabled(Feature feature) => + _enabledByKey[feature.key] ?? feature.defaultEnabled; +} diff --git a/lib/feature/feature_flags/feature_flags_repository.dart b/lib/feature/feature_flags/feature_flags_repository.dart new file mode 100644 index 0000000..2e75815 --- /dev/null +++ b/lib/feature/feature_flags/feature_flags_repository.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart' show rootBundle; + +import 'feature_flags.dart'; + +/// Lädt die Feature-Schalter aus der mitgelieferten Asset-Datei. +class FeatureFlagsRepository { + const FeatureFlagsRepository({this.assetPath = 'assets/feature_flags.json'}); + + final String assetPath; + + Future load() async { + final json = jsonDecode(await rootBundle.loadString(assetPath)); + if (json is! Map) { + throw const FormatException('feature_flags.json ist kein JSON-Objekt.'); + } + return FeatureFlags.fromJson(json); + } +} diff --git a/lib/feature/feature_flags/feature_gate.dart b/lib/feature/feature_flags/feature_gate.dart new file mode 100644 index 0000000..f848f84 --- /dev/null +++ b/lib/feature/feature_flags/feature_gate.dart @@ -0,0 +1,22 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'feature.dart'; +import 'feature_flags.dart'; + +/// Zeigt [child] nur, wenn [feature] aktiviert ist — sonst nichts (kein +/// Platzhalter, kein Abstand). Die Schalter kommen aus dem [FeatureFlags], +/// das oberhalb per `RepositoryProvider` bereitsteht. +class FeatureGate extends StatelessWidget { + const FeatureGate({super.key, required this.feature, required this.child}); + + final Feature feature; + final Widget child; + + @override + Widget build(BuildContext context) { + return context.read().isEnabled(feature) + ? child + : const SizedBox.shrink(); + } +} diff --git a/lib/widget/app.dart b/lib/widget/app.dart index 52dce90..5aa3c31 100644 --- a/lib/widget/app.dart +++ b/lib/widget/app.dart @@ -23,6 +23,7 @@ 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_state.dart'; +import 'package:hl_lieferservice/feature/feature_flags/feature_flags.dart'; import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart'; import 'package:hl_lieferservice/widget/operations/presentation/operation_view_enforcer.dart'; @@ -49,6 +50,9 @@ class _DeliveryAppState extends State { // API-Client. Kein Bloc hält Daten der alten Umgebung. key: ValueKey('environment-${state.active.id}'), providers: [ + // Feature-Schalter aus `assets/feature_flags.json`; Widgets lesen + // sie über `FeatureGate` bzw. `context.read()`. + RepositoryProvider.value(value: state.featureFlags), BlocProvider(create: (context) => NavigationBloc()), BlocProvider(create: (context) => OperationBloc()), BlocProvider( diff --git a/pubspec.yaml b/pubspec.yaml index 40c7ac8..be9b421 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -107,6 +107,7 @@ flutter: - assets/holzleitner_Logo_2017_RZ_transparent.png - assets/holzleitner_Logo_2017_RZ.png - assets/hl_server_config.json + - assets/feature_flags.json # To add assets to your application, add an assets section, like this: # assets: diff --git a/test/environment_settings_page_test.dart b/test/environment_settings_page_test.dart index 822ca3e..e45ea63 100644 --- a/test/environment_settings_page_test.dart +++ b/test/environment_settings_page_test.dart @@ -10,6 +10,8 @@ import 'package:hl_lieferservice/bloc/app_states.dart'; import 'package:hl_lieferservice/data/network/backend_config.dart'; import 'package:hl_lieferservice/data/network/backend_environment.dart'; import 'package:hl_lieferservice/data/network/environment_repository.dart'; +import 'package:hl_lieferservice/feature/feature_flags/feature_flags.dart'; +import 'package:hl_lieferservice/feature/feature_flags/feature_flags_repository.dart'; import 'package:hl_lieferservice/feature/settings/presentation/environment_settings_page.dart'; /// Liest die echte Asset-Config, speichert die Auswahl im Speicher. @@ -31,6 +33,14 @@ class _FakeRepository extends EnvironmentRepository { Future saveSelectedId(String id) async => selectedId = id; } +/// Ohne Asset-Bundle: leere Schalter (alle Features auf ihrem Default). +class _NoFlags extends FeatureFlagsRepository { + const _NoFlags(); + + @override + Future load() async => const FeatureFlags({}); +} + void main() { late _FakeRepository repo; late List activated; @@ -39,15 +49,20 @@ void main() { setUp(() { repo = _FakeRepository(null); activated = []; - bloc = AppBloc(repository: repo, activate: activated.add); + bloc = AppBloc( + repository: repo, + activate: activated.add, + featureFlagsRepository: const _NoFlags(), + ); }); tearDown(() => bloc.close()); test('ohne gespeicherte Wahl startet die Default-Umgebung', () async { bloc.add(AppLoadConfig(path: 'hl_server_config.json')); - final state = await bloc.stream.firstWhere((s) => s is AppConfigLoaded) - as AppConfigLoaded; + final state = + await bloc.stream.firstWhere((s) => s is AppConfigLoaded) + as AppConfigLoaded; expect(state.active.id, 'production'); expect(activated.single.apiBaseUrl, 'http://192.168.1.9:3000'); @@ -56,14 +71,16 @@ void main() { test('gespeicherte, aber unbekannte Umgebung fällt auf Default', () async { repo.selectedId = 'gibt-es-nicht'; bloc.add(AppLoadConfig(path: 'hl_server_config.json')); - final state = await bloc.stream.firstWhere((s) => s is AppConfigLoaded) - as AppConfigLoaded; + final state = + await bloc.stream.firstWhere((s) => s is AppConfigLoaded) + as AppConfigLoaded; expect(state.active.id, 'production'); }); - testWidgets('Card antippen wechselt Umgebung, speichert und schließt', - (tester) async { + testWidgets('Card antippen wechselt Umgebung, speichert und schließt', ( + tester, + ) async { await tester.runAsync(() async { bloc.add(AppLoadConfig(path: 'hl_server_config.json')); await bloc.stream.firstWhere((s) => s is AppConfigLoaded); @@ -74,16 +91,18 @@ void main() { value: bloc, child: MaterialApp( home: Builder( - builder: (context) => Scaffold( - body: TextButton( - onPressed: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const EnvironmentSettingsPage(), + builder: + (context) => Scaffold( + body: TextButton( + onPressed: + () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const EnvironmentSettingsPage(), + ), + ), + child: const Text('open'), ), ), - child: const Text('open'), - ), - ), ), ), ), diff --git a/test/feature_flags_test.dart b/test/feature_flags_test.dart new file mode 100644 index 0000000..37b2941 --- /dev/null +++ b/test/feature_flags_test.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:hl_lieferservice/feature/feature_flags/feature.dart'; +import 'package:hl_lieferservice/feature/feature_flags/feature_flags.dart'; +import 'package:hl_lieferservice/feature/feature_flags/feature_gate.dart'; + +Map _asset() => + jsonDecode(File('assets/feature_flags.json').readAsStringSync()) + as Map; + +void main() { + group('assets/feature_flags.json', () { + test('enthält jeden Feature-Key genau so, wie er im Code steht', () { + final keys = (_asset()['features'] as Map).keys.toSet(); + for (final f in Feature.values) { + expect(keys, contains(f.key), reason: 'Key fehlt: ${f.key}'); + } + }); + + test('Gutschriften-Abschnitt ist aktuell deaktiviert', () { + final flags = FeatureFlags.fromJson(_asset()); + expect(flags.isEnabled(Feature.articlesCreditSection), isFalse); + }); + }); + + group('FeatureFlags.fromJson', () { + test('fehlender Key fällt auf den Default zurück', () { + final flags = FeatureFlags.fromJson({'features': {}}); + for (final f in Feature.values) { + expect(flags.isEnabled(f), f.defaultEnabled); + } + }); + + test('unbekannte Keys werden ignoriert', () { + final flags = FeatureFlags.fromJson({ + 'features': { + 'gibt.es.nicht': {'enabled': true}, + 'articles.credit_section': {'enabled': false}, + }, + }); + expect(flags.isEnabled(Feature.articlesCreditSection), isFalse); + }); + + test('kein bool bei enabled ist ein Fehler', () { + expect( + () => FeatureFlags.fromJson({ + 'features': { + 'articles.credit_section': {'enabled': 'nein'}, + }, + }), + throwsFormatException, + ); + }); + }); + + group('FeatureGate', () { + Future pumpGate(WidgetTester tester, {required bool enabled}) { + return tester.pumpWidget( + RepositoryProvider.value( + value: FeatureFlags({Feature.articlesCreditSection.key: enabled}), + child: const MaterialApp( + home: FeatureGate( + feature: Feature.articlesCreditSection, + child: Text('Gutschriften'), + ), + ), + ), + ); + } + + testWidgets('zeigt den Inhalt bei aktivem Feature', (tester) async { + await pumpGate(tester, enabled: true); + expect(find.text('Gutschriften'), findsOneWidget); + }); + + testWidgets('blendet den Inhalt bei inaktivem Feature aus', (tester) async { + await pumpGate(tester, enabled: false); + expect(find.text('Gutschriften'), findsNothing); + }); + }); +}