diff --git a/example/test/home_page_test.dart b/example/test/home_page_test.dart index f16625b5..f1219540 100644 --- a/example/test/home_page_test.dart +++ b/example/test/home_page_test.dart @@ -11,6 +11,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + loadProviders(); + group('HomePage tests | ', () { testWidgets( 'correct UI', @@ -77,7 +79,10 @@ void main() { expect(graphQLTileFinder, findsOneWidget); await tester.tap(graphQLTileFinder); - await tester.pumpAndSettle(); + // pumpAndSettle times out here; as the page has non-deterministic loading indicator + // so pumping each frame individually + await tester.pump(); + await tester.pump(); expect(find.byType(CountryUI), findsOneWidget); }, diff --git a/packages/clean_framework/example/lib/main.dart b/packages/clean_framework/example/lib/main.dart index ee1d00df..37929a5c 100644 --- a/packages/clean_framework/example/lib/main.dart +++ b/packages/clean_framework/example/lib/main.dart @@ -3,18 +3,8 @@ import 'package:clean_framework_example/providers.dart'; import 'package:clean_framework_example/routing/routes.dart'; import 'package:clean_framework_router/clean_framework_router.dart'; import 'package:flutter/material.dart'; -import 'package:stack_trace/stack_trace.dart' as stack_trace; - -final container = ProviderContainer(); void main() { - FlutterError.demangleStackTrace = (StackTrace stack) { - if (stack is stack_trace.Trace) return stack.vmTrace; - if (stack is stack_trace.Chain) return stack.toTrace().vmTrace; - return stack; - }; - initializeExternalInterfaces(container); - runApp(const MyApp()); } @@ -23,13 +13,16 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return ProviderScope( - parent: container, + return AppProviderScope( + externalInterfaceProviders: [ + pokemonExternalInterfaceProvider, + ], child: AppRouterScope( create: PokeRouter.new, builder: (context) { return MaterialApp.router( title: 'Clean Framework Example', + routerConfig: context.router.config, theme: ThemeData.from( colorScheme: ColorScheme.fromSeed(seedColor: Colors.green), useMaterial3: true, @@ -42,7 +35,6 @@ class MyApp extends StatelessWidget { useMaterial3: true, ), themeMode: ThemeMode.dark, - routerConfig: context.router.config, ); }, ), diff --git a/packages/clean_framework/example/lib/providers.dart b/packages/clean_framework/example/lib/providers.dart index 52e6540f..c62004dd 100644 --- a/packages/clean_framework/example/lib/providers.dart +++ b/packages/clean_framework/example/lib/providers.dart @@ -33,7 +33,3 @@ final pokemonExternalInterfaceProvider = ExternalInterfaceProvider( pokemonSpeciesGateway, ], ); - -void initializeExternalInterfaces(ProviderContainer container) { - pokemonExternalInterfaceProvider.initializeFor(container); -} diff --git a/packages/clean_framework/lib/src/core/app_provider_scope.dart b/packages/clean_framework/lib/src/core/app_provider_scope.dart new file mode 100644 index 00000000..27194db8 --- /dev/null +++ b/packages/clean_framework/lib/src/core/app_provider_scope.dart @@ -0,0 +1,81 @@ +import 'package:clean_framework/clean_framework_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stack_trace/stack_trace.dart'; + +class AppProviderScope extends StatelessWidget { + const AppProviderScope({ + super.key, + required this.child, + this.externalInterfaceProviders = const [], + this.overrides = const [], + this.observers, + this.parent, + }); + + final Widget child; + + final ProviderContainer? parent; + + /// The listeners that subscribes to changes on providers + /// stored on this [ProviderScope]. + final List? observers; + + /// Information on how to override a provider/family. + final List overrides; + + final List externalInterfaceProviders; + + @override + Widget build(BuildContext context) { + return ProviderScope( + parent: parent, + observers: observers, + overrides: overrides, + child: Builder( + builder: (context) { + return _ProviderInitializer( + container: ProviderScope.containerOf(context), + externalInterfaceProviders: externalInterfaceProviders, + child: child, + ); + }, + ), + ); + } +} + +class _ProviderInitializer extends StatefulWidget { + const _ProviderInitializer({ + required this.container, + required this.child, + required this.externalInterfaceProviders, + }); + + final ProviderContainer container; + final Widget child; + final List externalInterfaceProviders; + + @override + State<_ProviderInitializer> createState() => _ProviderInitializerState(); +} + +class _ProviderInitializerState extends State<_ProviderInitializer> { + @override + void initState() { + super.initState(); + + FlutterError.demangleStackTrace = (StackTrace stack) { + if (stack is Trace) return stack.vmTrace; + if (stack is Chain) return stack.toTrace().vmTrace; + return stack; + }; + + for (final provider in widget.externalInterfaceProviders) { + provider.initializeFor(widget.container); + } + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/packages/clean_framework/lib/src/core/core.dart b/packages/clean_framework/lib/src/core/core.dart index c19144c0..1f558294 100644 --- a/packages/clean_framework/lib/src/core/core.dart +++ b/packages/clean_framework/lib/src/core/core.dart @@ -1,3 +1,4 @@ +export 'app_provider_scope.dart'; export 'external_interface/external_interface.dart'; export 'external_interface/external_interface_provider.dart'; export 'external_interface/request.dart'; diff --git a/packages/clean_framework/lib/src/core/use_case/helpers/input_filter_map.dart b/packages/clean_framework/lib/src/core/use_case/helpers/input_filter_map.dart index f19235e7..9f48e580 100644 --- a/packages/clean_framework/lib/src/core/use_case/helpers/input_filter_map.dart +++ b/packages/clean_framework/lib/src/core/use_case/helpers/input_filter_map.dart @@ -9,7 +9,11 @@ extension InputFilterMapExtension on InputFilterMap { final processor = this[I]; if (processor == null) { - throw StateError('Input processor not defined for $I'); + throw StateError( + '\n\nInput filter not defined for "$I".\n' + 'Filters available for: ${keys.isEmpty ? 'none' : keys.join(', ')}\n' + 'Dependency: $E\n\n', + ); } return processor(input, entity); diff --git a/packages/clean_framework/lib/src/core/use_case/helpers/output_filter_map.dart b/packages/clean_framework/lib/src/core/use_case/helpers/output_filter_map.dart index 3fb56808..3fd3b7fc 100644 --- a/packages/clean_framework/lib/src/core/use_case/helpers/output_filter_map.dart +++ b/packages/clean_framework/lib/src/core/use_case/helpers/output_filter_map.dart @@ -10,8 +10,9 @@ extension OutputFilterMapExtension on OutputFilterMap { if (builder == null) { throw StateError( - 'Output filter not defined for "$O".\n' - 'Filters available for: ${keys.join(', ')}', + '\n\nOutput filter not defined for "$O".\n' + 'Filters available for: ${keys.isEmpty ? 'none' : keys.join(', ')}\n' + 'Dependency: $E\n\n', ); } diff --git a/packages/clean_framework/lib/src/core/use_case/helpers/request_subscription_map.dart b/packages/clean_framework/lib/src/core/use_case/helpers/request_subscription_map.dart index ac14d4ad..cb0e2933 100644 --- a/packages/clean_framework/lib/src/core/use_case/helpers/request_subscription_map.dart +++ b/packages/clean_framework/lib/src/core/use_case/helpers/request_subscription_map.dart @@ -15,7 +15,7 @@ extension RequestSubscriptionMapExtension on RequestSubscriptionMap { void add(RequestSubscription subscription) { if (this[O] != null) { - throw StateError('A subscription for $O already exists'); + throw StateError('A subscription for $O already exists.'); } this[O] = subscription; @@ -27,8 +27,28 @@ extension RequestSubscriptionMapExtension final subscription = this[O]; if (subscription == null) { - return Either.left( - NoSubscriptionFailureInput(), + throw StateError( + '\n\nNo subscription for "$O" exists.\n\n' + 'Please follow the steps below in order to fix this issue:\n' + '1. Ensure that the use case that requests "$O" is attached ' + 'the appropriate gateway.\n' + ' AppropriateGatewayProvider(\n' + ' ...,\n' + ' useCases: [<>],\n' + ' )\n' + '2. Ensure that the gateway that belongs to "$O" is attached ' + 'the appropriate external interface.\n' + ' AppropriateExternalInterfaceProvider(\n' + ' ...,\n' + ' gateways: [<>],\n' + ' )\n' + '3. Ensure that the associated external interface is initialized.\n' + ' AppProviderScope(\n' + ' ...,\n' + ' externalInterfaceProviders: [\n' + ' <>,\n' + ' ],\n' + ' )\n', ); } diff --git a/packages/clean_framework/pubspec.yaml b/packages/clean_framework/pubspec.yaml index fdce4214..599d6b70 100644 --- a/packages/clean_framework/pubspec.yaml +++ b/packages/clean_framework/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: flutter_riverpod: ^2.1.3 meta: '>=1.8.0 <1.9.0' riverpod: ^2.1.3 + stack_trace: ">=1.10.0 <1.12.0" dev_dependencies: clean_framework_test: ^0.1.0 diff --git a/packages/clean_framework/test/core/use_case/use_case_test.dart b/packages/clean_framework/test/core/use_case/use_case_test.dart index 97c469a4..c14033c7 100644 --- a/packages/clean_framework/test/core/use_case/use_case_test.dart +++ b/packages/clean_framework/test/core/use_case/use_case_test.dart @@ -87,15 +87,16 @@ void main() { ); test( - 'request fails if there is no appropriate subscription present', + 'throws if there is no appropriate subscription present', () async { - await useCase.request( - TestGatewayOutput(name: 'World'), - onSuccess: (success) => TestEntity(foo: success.message), - onFailure: (failure) => TestEntity(foo: 'Hello Anonymous!'), + expect( + () => useCase.request( + TestGatewayOutput(name: 'World'), + onSuccess: (success) => TestEntity(), + onFailure: (failure) => TestEntity(), + ), + throwsStateError, ); - - expect(useCase.entity, TestEntity(foo: 'Hello Anonymous!')); }, );