import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; import 'package:provider/src/proxy_provider.dart' show NumericProxyProvider, Void; import 'common.dart'; Finder findProvider() => find.byWidgetPredicate( // comparing `runtimeType` instead of using `is` because `is` accepts subclasses but InheritedWidgets don't. (widget) => widget.runtimeType == typeOf>()); void main() { final a = A(); final b = B(); final c = C(); final d = D(); final e = E(); final f = F(); final combinedConsumerMock = ConsumerBuilderMock(); setUp(() => when(combinedConsumerMock(any)).thenReturn(Container())); tearDown(() { clearInteractions(combinedConsumerMock); }); final mockConsumer = Consumer( builder: (context, combined, child) => combinedConsumerMock(combined), ); group('ProxyProvider', () { final combiner = CombinerMock(); setUp(() { when(combiner(any, any, any)).thenAnswer((Invocation invocation) { return Combined( invocation.positionalArguments.first as BuildContext, invocation.positionalArguments[2] as Combined, invocation.positionalArguments[1] as A, ); }); }); tearDown(() => clearInteractions(combiner)); Finder findProxyProvider() => find.byWidgetPredicate( (widget) => widget is NumericProxyProvider, ); testWidgets('throws if the provided value is a Listenable/Stream', (tester) async { await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), ProxyProvider( builder: (_, __, ___) => MyListenable(), ) ], child: Container(), ), ); expect(tester.takeException(), isFlutterError); await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), ProxyProvider( builder: (_, __, ___) => MyStream(), ) ], child: Container(), ), ); expect(tester.takeException(), isFlutterError); }); testWidgets('debugCheckInvalidValueType can be disabled', (tester) async { final previous = Provider.debugCheckInvalidValueType; Provider.debugCheckInvalidValueType = null; addTearDown(() => Provider.debugCheckInvalidValueType = previous); await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), ProxyProvider( builder: (_, __, ___) => MyListenable(), ) ], child: Container(), ), ); await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), ProxyProvider( builder: (_, __, ___) => MyStream(), ) ], child: Container(), ), ); }); testWidgets('initialBuilder creates initial value', (tester) async { final initialBuilder = ValueBuilderMock(); final key = GlobalKey(); when(initialBuilder(any)).thenReturn(Combined(null, null, null)); await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), ProxyProvider( key: key, initialBuilder: initialBuilder, builder: combiner, ) ], child: mockConsumer, ), ); final details = verify(initialBuilder(captureAny))..called(1); expect(details.captured.first, equals(key.currentContext)); verify(combiner(key.currentContext, a, Combined(null, null, null))); }); testWidgets('consume another providers', (tester) async { await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), ProxyProvider( builder: combiner, ) ], child: mockConsumer, ), ); final context = tester.element(findProxyProvider()); verify(combinedConsumerMock(Combined(context, null, a))).called(1); verifyNoMoreInteractions(combinedConsumerMock); verify(combiner(context, a, null)).called(1); verifyNoMoreInteractions(combiner); }); test('throws if builder is null', () { // ignore: prefer_const_constructors expect(() => ProxyProvider(builder: null), throwsAssertionError); }); testWidgets('rebuild descendants if value change', (tester) async { await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), ProxyProvider( builder: combiner, ) ], child: mockConsumer, ), ); final a2 = A(); await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a2), ProxyProvider( builder: combiner, ) ], child: mockConsumer, ), ); final context = tester.element(findProxyProvider()); verifyInOrder([ combiner(context, a, null), combinedConsumerMock(Combined(context, null, a)), combiner(context, a2, Combined(context, null, a)), combinedConsumerMock(Combined(context, Combined(context, null, a), a2)), ]); verifyNoMoreInteractions(combiner); verifyNoMoreInteractions(combinedConsumerMock); }); testWidgets('call dispose when unmounted with the latest result', (tester) async { final dispose = DisposerMock(); final dispose2 = DisposerMock(); await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), ProxyProvider(builder: combiner, dispose: dispose) ], child: mockConsumer, ), ); final a2 = A(); // ProxyProvider creates a new Combined instance await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a2), ProxyProvider(builder: combiner, dispose: dispose2) ], child: mockConsumer, ), ); final context = tester.element(findProxyProvider()); await tester.pumpWidget(Container()); verifyZeroInteractions(dispose); verify( dispose2(context, Combined(context, Combined(context, null, a), a2))); }); testWidgets("don't rebuild descendants if value doesn't change", (tester) async { await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), ProxyProvider( builder: (c, a, p) => combiner(c, a, null), ) ], child: mockConsumer, ), ); await tester.pumpWidget( MultiProvider( providers: [ Provider.value( value: a, updateShouldNotify: (A _, A __) => true, ), ProxyProvider( builder: (c, a, p) { combiner(c, a, p); return p; }, ) ], child: mockConsumer, ), ); final context = tester.element(findProxyProvider()); verifyInOrder([ combiner(context, a, null), combinedConsumerMock(Combined(context, null, a)), combiner(context, a, Combined(context, null, a)), ]); verifyNoMoreInteractions(combiner); verifyNoMoreInteractions(combinedConsumerMock); }); testWidgets('pass down updateShouldNotify', (tester) async { var buildCount = 0; final child = Builder(builder: (context) { buildCount++; return Text( '$buildCount ${Provider.of(context)}', textDirection: TextDirection.ltr, ); }); final shouldNotify = UpdateShouldNotifyMock(); when(shouldNotify('Hello', 'Hello')).thenReturn(false); await tester.pumpWidget(MultiProvider( providers: [ Provider.value( value: 'Hello', updateShouldNotify: (_, __) => true), ProxyProvider( builder: (_, value, __) => value, updateShouldNotify: shouldNotify, ), ], child: child, )); await tester.pumpWidget(MultiProvider( providers: [ Provider.value( value: 'Hello', updateShouldNotify: (_, __) => true), ProxyProvider( builder: (_, value, __) => value, updateShouldNotify: shouldNotify, ), ], child: child, )); verify(shouldNotify('Hello', 'Hello')).called(1); verifyNoMoreInteractions(shouldNotify); expect(find.text('2 Hello'), findsNothing); expect(find.text('1 Hello'), findsOneWidget); }); testWidgets('works with MultiProvider', (tester) async { final key = GlobalKey(); await tester.pumpWidget(MultiProvider( providers: [ Provider.value(value: a), ProxyProvider(builder: (c, a, p) => Combined(c, p, a)), ], child: Container(key: key), )); final context = tester.element(findProxyProvider()); expect( Provider.of(key.currentContext), Combined(context, null, a), ); }); test('works with MultiProvider #2', () { final provider = ProxyProvider( key: const Key('42'), initialBuilder: (_) => null, builder: (_, __, ___) => null, updateShouldNotify: (_, __) => null, dispose: (_, __) {}, child: Container(), ); var child2 = Container(); final clone = provider.cloneWithChild(child2); expect(clone.child, equals(child2)); expect(clone.key, equals(provider.key)); expect(clone.initialBuilder, equals(provider.initialBuilder)); expect(clone.builder, equals(provider.builder)); expect(clone.updateShouldNotify, equals(provider.updateShouldNotify)); expect(clone.dispose, equals(provider.dispose)); // expect(clone.providerBuilder, equals(provider.providerBuilder)); }); // useful for libraries such as Mobx where events are synchronously dispatched testWidgets( 'builder callback can trigger descendants setState synchronously', (tester) async { var statefulBuildCount = 0; void Function(VoidCallback) setState; final statefulBuilder = StatefulBuilder(builder: (_, s) { setState = s; statefulBuildCount++; return Container(); }); await tester.pumpWidget(MultiProvider( providers: [ Provider.value(value: a), ProxyProvider(builder: (c, a, p) => Combined(c, p, a)), ], child: statefulBuilder, )); await tester.pumpWidget(MultiProvider( providers: [ Provider.value(value: A()), ProxyProvider(builder: (c, a, p) { setState(() {}); return Combined(c, p, a); }), ], child: statefulBuilder, )); expect( statefulBuildCount, 2, reason: 'builder must not be called asynchronously', ); }); }); group('ProxyProvider variants', () { Finder findProxyProvider() => find.byWidgetPredicate( (widget) => widget is NumericProxyProvider, ); testWidgets('ProxyProvider2', (tester) async { await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), Provider.value(value: b), Provider.value(value: c), Provider.value(value: d), Provider.value(value: e), Provider.value(value: f), ProxyProvider2( initialBuilder: (_) => Combined(null, null, null), builder: (context, a, b, previous) => Combined(context, previous, a, b), ) ], child: mockConsumer, ), ); final context = tester.element(findProxyProvider()); verify( combinedConsumerMock( Combined(context, Combined(null, null, null), a, b), ), ).called(1); }); testWidgets('ProxyProvider3', (tester) async { await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), Provider.value(value: b), Provider.value(value: c), Provider.value(value: d), Provider.value(value: e), Provider.value(value: f), ProxyProvider3( initialBuilder: (_) => Combined(null, null, null), builder: (context, a, b, c, previous) => Combined(context, previous, a, b, c), ) ], child: mockConsumer, ), ); final context = tester.element(findProxyProvider()); verify( combinedConsumerMock( Combined(context, Combined(null, null, null), a, b, c), ), ).called(1); }); testWidgets('ProxyProvider4', (tester) async { await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), Provider.value(value: b), Provider.value(value: c), Provider.value(value: d), Provider.value(value: e), Provider.value(value: f), ProxyProvider4( initialBuilder: (_) => Combined(null, null, null), builder: (context, a, b, c, d, previous) => Combined(context, previous, a, b, c, d), ) ], child: mockConsumer, ), ); final context = tester.element(findProxyProvider()); verify( combinedConsumerMock( Combined(context, Combined(null, null, null), a, b, c, d), ), ).called(1); }); testWidgets('ProxyProvider5', (tester) async { await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), Provider.value(value: b), Provider.value(value: c), Provider.value(value: d), Provider.value(value: e), Provider.value(value: f), ProxyProvider5( initialBuilder: (_) => Combined(null, null, null), builder: (context, a, b, c, d, e, previous) => Combined(context, previous, a, b, c, d, e), ) ], child: mockConsumer, ), ); final context = tester.element(findProxyProvider()); verify( combinedConsumerMock( Combined(context, Combined(null, null, null), a, b, c, d, e, null), ), ).called(1); }); testWidgets('ProxyProvider6', (tester) async { await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: a), Provider.value(value: b), Provider.value(value: c), Provider.value(value: d), Provider.value(value: e), Provider.value(value: f), ProxyProvider6( initialBuilder: (_) => Combined(null, null, null), builder: (context, a, b, c, d, e, f, previous) => Combined(context, previous, a, b, c, d, e, f), ) ], child: mockConsumer, ), ); final context = tester.element(findProxyProvider()); verify( combinedConsumerMock( Combined(context, Combined(null, null, null), a, b, c, d, e, f), ), ).called(1); }); }); }