| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:lib.widgets/model.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:test/test.dart' as package_test; |
| |
| const ProviderScope scope1 = ProviderScope('scope1'); |
| const ProviderScope scope2 = ProviderScope('scope2'); |
| |
| void main() { |
| Providers providers; |
| |
| setUp(() { |
| providers = Providers(); |
| }); |
| |
| group('Providers', () { |
| BuildContext buildContext; |
| |
| setUp(() { |
| buildContext = MockBuildContext(); |
| }); |
| |
| test('can set and retreive a single value', () { |
| const value = 'value'; |
| const otherValue = 'otherValue'; |
| |
| providers.provideValue(value); |
| final provider = providers.getFromType(String); |
| expect(provider.get(buildContext), value); |
| |
| providers.provideValue(otherValue); |
| final otherProvider = providers.getFromType(String); |
| expect(otherProvider.get(buildContext), otherValue); |
| }); |
| test('can provide and retreive various kinds of providers', () async { |
| final streamController = StreamController<String>.broadcast(); |
| int functionCounter = 0; |
| int factoryCounter = 0; |
| |
| providers |
| ..provide(Provider.withFactory((buildContext) { |
| return factoryCounter++; |
| })) |
| ..provideAll({ |
| String: Provider<String>.stream(streamController.stream), |
| SampleClass: Provider<SampleClass>.function((buildContext) { |
| final value = SampleClass('function $functionCounter'); |
| functionCounter++; |
| return value; |
| }), |
| }) |
| ..provide(SampleProvider()); |
| |
| // Must wait one async cycle for value to propagate. |
| streamController.add('stream'); |
| await Future.delayed(Duration.zero); |
| |
| expect(providers.getFromType(String).get(buildContext), 'stream'); |
| expect(providers.getFromType(String).get(buildContext), 'stream'); |
| |
| // Must wait one async cycle for value to propagate. |
| streamController.add('stream2'); |
| await Future.delayed(Duration.zero); |
| |
| expect(providers.getFromType(String).get(buildContext), 'stream2'); |
| |
| expect(providers.getFromType(SampleClass).get(buildContext).value, |
| 'function 0'); |
| expect(providers.getFromType(SampleClass).get(buildContext).value, |
| 'function 0'); |
| |
| expect(providers.getFromType(int).get(buildContext), 0); |
| expect(providers.getFromType(int).get(buildContext), 1); |
| |
| expect(providers.getFromType(double).get(buildContext), 1.1); |
| |
| // Copied providers should have the same providers as original. |
| final copiedProviders = Providers()..provideFrom(providers); |
| expect(copiedProviders.getFromType(String).get(buildContext), 'stream2'); |
| expect(copiedProviders.getFromType(SampleClass).get(buildContext).value, |
| 'function 0'); |
| expect(copiedProviders.getFromType(int).get(buildContext), 2); |
| expect(copiedProviders.getFromType(double).get(buildContext), 1.1); |
| |
| await streamController.close(); |
| }); |
| test('Throws errors when type incorrect', () { |
| // incorrect type |
| expect(() => providers.provideAll({String: Provider.value(32)}), |
| throwsA(package_test.TypeMatcher<ArgumentError>())); |
| // provider type not inferred |
| expect(() => providers.provideAll({String: Provider.value('')}), |
| throwsA(package_test.TypeMatcher<ArgumentError>())); |
| }); |
| |
| test('can handle multiple scopes', () { |
| providers |
| ..provideValue(1, scope: scope1) |
| ..provide(Provider.value(2), scope: scope2) |
| ..provideValue(360); |
| |
| expect(providers.getFromType(int).get(buildContext), 360); |
| expect(providers.getFromType(int, scope: scope1).get(buildContext), 1); |
| expect(providers.getFromType(int, scope: scope2).get(buildContext), 2); |
| |
| final other = Providers()..provideFrom(providers); |
| expect(other.getFromType(int).get(buildContext), 360); |
| expect(other.getFromType(int, scope: scope1).get(buildContext), 1); |
| expect(other.getFromType(int, scope: scope2).get(buildContext), 2); |
| |
| // overwriting in the same scope |
| providers.provideValue(3, scope: scope1); |
| expect(providers.getFromType(int, scope: scope1).get(buildContext), 3); |
| |
| providers.provideAll( |
| {double: Provider<double>.function((buildContext) => 1.0)}, |
| scope: scope2); |
| |
| expect( |
| providers.getFromType(double, scope: scope2).get(buildContext), 1.0); |
| }); |
| }); |
| |
| group('Provide', () { |
| FakeModel model; |
| SampleClass sampleClass; |
| ValueNotifier<String> notifier; |
| StreamController<int> broadcastController; |
| StreamController<double> singleStreamController; |
| |
| setUp(() async { |
| model = FakeModel(); |
| sampleClass = SampleClass('value'); |
| notifier = ValueNotifier<String>('valueNotifier'); |
| broadcastController = StreamController<int>.broadcast(); |
| singleStreamController = StreamController<double>(); |
| |
| providers |
| ..provideValue(model) |
| ..provideValue(notifier) |
| ..provide(Provider.stream(broadcastController.stream)) |
| ..provide(Provider.stream(singleStreamController.stream), scope: scope1) |
| ..provideValue(sampleClass, scope: scope2) |
| // a provider that uses other provided values when accessed |
| ..provide(Provider.function((context) => SampleClass( |
| Provide.value<SampleClass>(context, scope: scope2).value))); |
| |
| broadcastController.add(1); |
| singleStreamController.add(1.0); |
| |
| // wait for the values to propagate |
| await Future.delayed(Duration.zero); |
| }); |
| |
| testWidgets('rebuilds dependent provide widget when providers change', |
| (tester) async { |
| final intController = StreamController<int>(); |
| |
| final otherProviders = Providers() |
| ..provide(Provider.stream(intController.stream)); |
| int childBuilds = 0; |
| |
| intController.add(2); |
| |
| int providedValue; |
| final provide = Provide<int>( |
| builder: (context, child, value) { |
| providedValue = value; |
| childBuilds++; |
| return Container(); |
| }, |
| ); |
| |
| await tester |
| .pumpWidget(ProviderNode(providers: providers, child: provide)); |
| |
| expect(childBuilds, 1); |
| expect(providedValue, 1); |
| |
| await tester |
| .pumpWidget(ProviderNode(providers: providers, child: provide)); |
| |
| expect(childBuilds, 1); |
| expect(providedValue, 1); |
| |
| await tester |
| .pumpWidget(ProviderNode(providers: otherProviders, child: provide)); |
| |
| expect(childBuilds, 2); |
| expect(providedValue, 2); |
| |
| await intController.close(); |
| }); |
| |
| testWidgets('Rebuilds dependent provide.value when providers change', |
| (tester) async { |
| final otherProviders = Providers()..provideValue<int>(2); |
| int childBuilds = 0; |
| |
| int providedValue; |
| final provide = CallbackWidget( |
| (context) { |
| providedValue = Provide.value<int>(context); |
| childBuilds++; |
| return Container(); |
| }, |
| ); |
| |
| await tester |
| .pumpWidget(ProviderNode(providers: providers, child: provide)); |
| |
| expect(childBuilds, 1); |
| expect(providedValue, 1); |
| |
| await tester |
| .pumpWidget(ProviderNode(providers: providers, child: provide)); |
| |
| expect(childBuilds, 1); |
| expect(providedValue, 1); |
| |
| await tester |
| .pumpWidget(ProviderNode(providers: otherProviders, child: provide)); |
| |
| expect(childBuilds, 2); |
| expect(providedValue, 2); |
| }); |
| |
| testWidgets('can get static values', (tester) async { |
| await tester.pumpWidget(ProviderNode( |
| providers: providers, |
| child: TesterWidget( |
| expectedInt: 1, |
| expectedDouble: 1.0, |
| expectedSampleClass: sampleClass, |
| expectedModel: model, |
| expectedString: 'valueNotifier'))); |
| }); |
| |
| testWidgets('can get listened values', (tester) async { |
| bool buildCalled = false; |
| int expectedValue = 0; |
| |
| final widget = ProviderNode( |
| providers: providers, |
| child: Provide<FakeModel>(builder: (context, child, value) { |
| expect(value.value, expectedValue); |
| buildCalled = true; |
| return Container(); |
| }), |
| ); |
| |
| await tester.pumpWidget(widget); |
| expect(buildCalled, isTrue); |
| |
| buildCalled = false; |
| expectedValue++; |
| model.increment(); |
| |
| await tester.pumpAndSettle(); |
| expect(buildCalled, isTrue); |
| }); |
| |
| testWidgets('can get multi level dependencies', (tester) async { |
| bool buildCalled = false; |
| String expectedValue = sampleClass.value; |
| |
| final widget = ProviderNode( |
| providers: providers, |
| child: Provide<SampleClass>(builder: (context, child, value) { |
| expect(value.value, expectedValue); |
| buildCalled = true; |
| return Container(); |
| }), |
| ); |
| |
| await tester.pumpWidget(widget); |
| expect(buildCalled, isTrue); |
| }); |
| |
| testWidgets('can get listened streams', (tester) async { |
| bool buildCalled = false; |
| double expectedValue = 1.0; |
| |
| final widget = ProviderNode( |
| providers: providers, |
| child: Provide<double>( |
| scope: scope1, |
| builder: (context, child, value) { |
| expect(value, expectedValue); |
| buildCalled = true; |
| return Container(); |
| }), |
| ); |
| |
| await tester.pumpWidget(widget); |
| expect(buildCalled, isTrue); |
| |
| buildCalled = false; |
| expectedValue = 2.0; |
| singleStreamController.add(2.0); |
| await tester.pumpAndSettle(); |
| expect(buildCalled, isTrue); |
| }); |
| |
| testWidgets('can get many listened values', (tester) async { |
| bool buildCalled = false; |
| |
| var expectedString = 'valueNotifier'; |
| var expectedInt = 1; |
| var expectedDouble = 1.0; |
| |
| final widget = ProviderNode( |
| providers: providers, |
| child: ProvideMulti( |
| requestedValues: [ |
| // This seems to be a bug; can't parse a generic type in an array literal? |
| ValueNotifier<String>('').runtimeType, |
| int, |
| ], |
| requestedScopedValues: { |
| scope1: [double] |
| }, |
| builder: (context, child, value) { |
| expect(value.get<ValueNotifier<String>>().value, expectedString); |
| expect(value.get<int>(), expectedInt); |
| expect(value.get<double>(scope: scope1), expectedDouble); |
| |
| buildCalled = true; |
| return Container(); |
| }), |
| ); |
| |
| await tester.pumpWidget(widget); |
| expect(buildCalled, isTrue); |
| |
| buildCalled = false; |
| expectedString = 'updated'; |
| notifier.value = 'updated'; |
| await tester.pumpAndSettle(); |
| expect(buildCalled, isTrue); |
| |
| buildCalled = false; |
| expectedInt = 2; |
| broadcastController.add(2); |
| await tester.pumpAndSettle(); |
| expect(buildCalled, isTrue); |
| |
| buildCalled = false; |
| expectedDouble = 2.0; |
| singleStreamController.add(2.0); |
| await tester.pumpAndSettle(); |
| expect(buildCalled, isTrue); |
| }); |
| |
| testWidgets('does not rebuild child', (tester) async { |
| int childBuilds = 0; |
| int builderBuilds = 0; |
| |
| final callbackChild = CallbackWidget((_) { |
| childBuilds++; |
| }); |
| |
| await tester.pumpWidget(ProviderNode( |
| providers: providers, |
| child: Provide<FakeModel>( |
| builder: (context, child, model) { |
| return CallbackWidget((_) { |
| builderBuilds++; |
| }, child: child); |
| }, |
| child: callbackChild))); |
| expect(childBuilds, 1); |
| expect(builderBuilds, 1); |
| |
| await tester.pumpAndSettle(); |
| expect(childBuilds, 1); |
| expect(builderBuilds, 1); |
| |
| model.increment(); |
| await tester.pumpAndSettle(); |
| expect(childBuilds, 1); |
| expect(builderBuilds, 2); |
| }); |
| |
| testWidgets('disposes when removed from tree', (tester) async { |
| final fakeModel2 = FakeModel2(); |
| |
| final build = ValueNotifier<bool>(true); |
| |
| providers |
| ..provide(Provider.function((buildContext) => model, dispose: true)) |
| ..provide( |
| Provider.function((buildContext) => fakeModel2, dispose: false)) |
| ..provideValue(build); |
| |
| expect(model.listenerCount, 0); |
| expect(fakeModel2.listenerCount, 0); |
| |
| await tester.pumpWidget(ProviderNode( |
| providers: providers, |
| child: Provide<ValueNotifier<bool>>( |
| builder: (context, child, value) => value.value |
| ? Provide<FakeModel2>( |
| builder: (ontext, child, model2) => |
| Provide<FakeModel>(builder: (context, child, model) { |
| return Container(); |
| })) |
| : Container(), |
| ))); |
| |
| expect(model.listenerCount, 1); |
| expect(fakeModel2.listenerCount, 1); |
| |
| build.value = false; |
| await tester.pumpAndSettle(); |
| expect(model.listenerCount, 0); |
| expect(fakeModel2.listenerCount, 1); |
| }); |
| |
| testWidgets('disposes when all removed from tree', (tester) async { |
| final fakeModel2 = FakeModel2(); |
| final build = ValueNotifier<bool>(true); |
| |
| providers |
| ..provide(Provider.function((buildContext) => model, dispose: true)) |
| ..provide( |
| Provider.function((buildContext) => fakeModel2, dispose: true)) |
| ..provideValue(build); |
| |
| await tester.pumpWidget(ProviderNode( |
| providers: providers, |
| child: Provide<ValueNotifier<bool>>( |
| builder: (context, child, value) => value.value |
| ? ProvideMulti( |
| requestedValues: [FakeModel, FakeModel2], |
| builder: (context, child, model) { |
| return Container(); |
| }) |
| : Container(), |
| ))); |
| |
| expect(model.listenerCount, 1); |
| expect(fakeModel2.listenerCount, 1); |
| |
| build.value = false; |
| await tester.pumpAndSettle(); |
| |
| expect(model.listenerCount, 0); |
| expect(fakeModel2.listenerCount, 0); |
| }); |
| |
| tearDown(() { |
| singleStreamController.close(); |
| broadcastController.close(); |
| }); |
| }); |
| } |
| |
| class MockBuildContext extends Mock implements BuildContext {} |
| |
| class SampleClass { |
| String value; |
| SampleClass(this.value); |
| } |
| |
| class SampleProvider extends TypedProvider<double> { |
| @override |
| double get(BuildContext context) => 1.1; |
| } |
| |
| class FakeModel extends Model { |
| int _value = 0; |
| |
| int get value => _value; |
| |
| void increment() { |
| _value++; |
| notifyListeners(); |
| } |
| } |
| |
| class FakeModel2 extends FakeModel {} |
| |
| class CallbackWidget extends StatelessWidget { |
| final void Function(BuildContext) callback; |
| final Widget child; |
| |
| const CallbackWidget(this.callback, {this.child}); |
| |
| @override |
| Widget build(BuildContext context) { |
| callback(context); |
| return child ?? Container(); |
| } |
| } |
| |
| class TesterWidget extends StatelessWidget { |
| final int expectedInt; |
| final double expectedDouble; |
| final String expectedString; |
| final SampleClass expectedSampleClass; |
| final FakeModel expectedModel; |
| |
| const TesterWidget( |
| {this.expectedDouble, |
| this.expectedSampleClass, |
| this.expectedInt, |
| this.expectedModel, |
| this.expectedString}); |
| |
| @override |
| Widget build(BuildContext context) { |
| expect(Provide.value<int>(context), expectedInt); |
| expect(Provide.value<double>(context, scope: scope1), expectedDouble); |
| expect(Provide.value<ValueNotifier<String>>(context).value, expectedString); |
| expect(Provide.value<FakeModel>(context), expectedModel); |
| expect(Provide.value<SampleClass>(context, scope: scope2), |
| expectedSampleClass); |
| |
| return Container(); |
| } |
| } |