diff --git a/lib/over_react.dart b/lib/over_react.dart index ba78a063f..60cc04777 100644 --- a/lib/over_react.dart +++ b/lib/over_react.dart @@ -65,6 +65,7 @@ export 'src/util/class_names.dart'; export 'src/util/constants_base.dart'; export 'src/util/css_value_util.dart'; export 'src/util/dom_util.dart'; +export 'src/util/equality.dart' show areMapsShallowIdentical; export 'src/util/event_helpers.dart'; export 'src/util/guid_util.dart'; export 'src/util/hoc.dart'; diff --git a/lib/src/component_declaration/component_base.dart b/lib/src/component_declaration/component_base.dart index 0cada1bc4..293e55b3d 100644 --- a/lib/src/component_declaration/component_base.dart +++ b/lib/src/component_declaration/component_base.dart @@ -541,7 +541,10 @@ abstract class UiProps extends MapBase // Use `identical` since it compiles down to `===` in dart2js instead of calling equality helper functions, // and we don't want to allow any object overriding `operator==` to claim it's equal to `_notSpecified`. if (identical(c1, notSpecified)) { - childArguments = []; + // Use a const list so that empty children prop values are always identical + // in the JS props, resulting in JS libraries (e.g., react-redux) and Dart code alike + // not marking props as having changed as a result of rerendering the ReactElement with a new list. + childArguments = const []; } else if (identical(c2, notSpecified)) { childArguments = [c1]; } else if (identical(c3, notSpecified)) { diff --git a/lib/src/over_react_redux/over_react_flux.dart b/lib/src/over_react_redux/over_react_flux.dart index 573cae183..b202f497e 100644 --- a/lib/src/over_react_redux/over_react_flux.dart +++ b/lib/src/over_react_redux/over_react_flux.dart @@ -15,10 +15,10 @@ import 'dart:async'; import 'dart:html'; -import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; +import 'package:over_react/src/util/equality.dart'; import 'package:redux/redux.dart' as redux; import 'package:w_flux/w_flux.dart' as flux; @@ -309,8 +309,6 @@ mixin InfluxStoreMixin on flux.Store { } } -bool _shallowMapEquality(Map a, Map b) => const MapEquality().equals(a, b); - /// A wrapper around the `connect` function that provides a similar API into a Flux store. /// /// This is primarily for use while transitioning _to_ `connect` and OverReact Redux. @@ -357,9 +355,9 @@ bool _shallowMapEquality(Map a, Map b) => const MapEquality().equals(a, b); /// If you do not provide [mergeProps], the wrapped component receives {...ownProps, ...stateProps, ...dispatchProps} /// by default. /// -/// - [areOwnPropsEqual] does a shallow Map equality check by default. -/// - [areStatePropsEqual] does a shallow Map equality check by default. -/// - [areMergedPropsEqual] does a shallow Map equality check by default. +/// - [areOwnPropsEqual] does an equality check using JS `===` (equivalent to [identical]) by default. +/// - [areStatePropsEqual] does a shallow Map equality check using JS `===` (equivalent to [identical]) by default. +/// - [areMergedPropsEqual] does a shallow Map equality check using JS `===` (equivalent to [identical]) by default. /// /// - [context] can be utilized to provide a custom context object created with `createContext`. /// [context] is how you can utilize multiple stores. While supported, this is not recommended. @@ -569,13 +567,13 @@ UiFactory Function(UiFactory) } /*--end usage of cases--*/ - if (areStatePropsEqual == null) { - const defaultAreStatePropsEqual = _shallowMapEquality; - const propHasher = CollectionLengthHasher(); - bool areStatePropsEqualWrapper(TProps nextProps, TProps prevProps) { - final result = defaultAreStatePropsEqual(nextProps, prevProps); + // In dev mode, if areStatePropsEqual is not specified, pass in a version + // that warns for common pitfall cases. + assert(() { + if (areStatePropsEqual == null) { + bool areStatePropsEqualWrapper(TProps nextProps, TProps prevProps) { + const propHasher = CollectionLengthHasher(); - assert(() { prevProps.forEach((key, value) { // If the value is the same instance, check if the instance has been mutated, // causing its hash to be updated @@ -592,14 +590,13 @@ UiFactory Function(UiFactory) } }); - return true; - }()); - - return result; + return areMapsShallowIdentical(nextProps, prevProps); + } + areStatePropsEqual = areStatePropsEqualWrapper; } - areStatePropsEqual = areStatePropsEqualWrapper; - } + return true; + }()); return connect( mapStateToProps: mapStateToProps, diff --git a/lib/src/over_react_redux/over_react_redux.dart b/lib/src/over_react_redux/over_react_redux.dart index c36e18868..108cc8927 100644 --- a/lib/src/over_react_redux/over_react_redux.dart +++ b/lib/src/over_react_redux/over_react_redux.dart @@ -18,16 +18,16 @@ library over_react_redux; import 'dart:html'; import 'dart:js_util' as js_util; -import 'package:meta/meta.dart'; -import 'package:over_react/src/component_declaration/component_base.dart' as component_base; -import 'package:over_react/src/component_declaration/builder_helpers.dart' as builder_helpers; -import 'package:collection/collection.dart'; + import 'package:js/js.dart'; +import 'package:memoize/memoize.dart'; +import 'package:meta/meta.dart'; import 'package:over_react/over_react.dart'; +import 'package:over_react/src/component_declaration/builder_helpers.dart' as builder_helpers; import 'package:over_react/src/component_declaration/component_type_checking.dart'; import 'package:react/react_client.dart'; -import 'package:react/react_client/react_interop.dart'; import 'package:react/react_client/js_backed_map.dart'; +import 'package:react/react_client/react_interop.dart'; import 'package:redux/redux.dart'; part 'over_react_redux.over_react.g.dart'; @@ -89,10 +89,10 @@ typedef dynamic Dispatcher(dynamic action); /// If you do not provide [mergeProps], the wrapped component receives {...ownProps, ...stateProps, ...dispatchProps} /// by default. /// -/// - [areStatesEqual] does a simple `==` check by default. -/// - [areOwnPropsEqual] does a shallow Map equality check by default. -/// - [areStatePropsEqual] does a shallow Map equality check by default. -/// - [areMergedPropsEqual] does a shallow Map equality check by default. +/// - [areStatesEqual] does an equality check using JS `===` (equivalent to [identical]) by default. +/// - [areOwnPropsEqual] does a shallow Map equality check using JS `===` (equivalent to [identical]) by default. +/// - [areStatePropsEqual] does a shallow Map equality check using JS `===` (equivalent to [identical]) by default. +/// - [areMergedPropsEqual] does a shallow Map equality check using JS `===` (equivalent to [identical]) by default. /// /// - [context] can be utilized to provide a custom context object created with `createContext`. /// [context] is how you can utilize multiple stores. While supported, this is not recommended. :P @@ -156,11 +156,6 @@ UiFactory Function(UiFactory) connect wrapWithConnect(UiFactory factory) { final dartComponentFactory = factory().componentFactory; final dartComponentClass = dartComponentFactory.type; @@ -230,19 +225,31 @@ UiFactory Function(UiFactory) connect areMergedPropsEqual(jsPropsToTProps(jsNext), jsPropsToTProps(jsPrev)); + final connectOptions = JsConnectOptions( + forwardRef: forwardRef, + pure: pure, + context: context?.jsThis ?? JsReactRedux.ReactReduxContext, + ); + // These can't be `null` in the JS object, so we conditionally define them + // so they won't exist in the object if we don't want to specify them. + if (areStatesEqual != null) { + connectOptions.areStatesEqual = allowInterop(handleAreStatesEqual); + } + if (areOwnPropsEqual != null) { + connectOptions.areOwnPropsEqual = allowInterop(handleAreOwnPropsEqual); + } + if (areStatePropsEqual != null) { + connectOptions.areStatePropsEqual = allowInterop(handleAreStatePropsEqual); + } + if (areMergedPropsEqual != null) { + connectOptions.areMergedPropsEqual = allowInterop(handleAreMergedPropsEqual); + } + final hoc = mockableJsConnect( mapStateToProps != null ? allowInteropWithArgCount(handleMapStateToProps, 1) : mapStateToPropsWithOwnProps != null ? allowInteropWithArgCount(handleMapStateToPropsWithOwnProps, 2) : null, mapDispatchToProps != null ? allowInteropWithArgCount(handleMapDispatchToProps, 1) : mapDispatchToPropsWithOwnProps != null ? allowInteropWithArgCount(handleMapDispatchToPropsWithOwnProps, 2) : null, mergeProps != null ? allowInterop(handleMergeProps) : null, - JsConnectOptions( - areStatesEqual: allowInterop(handleAreStatesEqual), - areOwnPropsEqual: allowInterop(handleAreOwnPropsEqual), - areStatePropsEqual: allowInterop(handleAreStatePropsEqual), - areMergedPropsEqual: allowInterop(handleAreMergedPropsEqual), - forwardRef: forwardRef, - pure: pure, - context: context?.jsThis ?? JsReactRedux.ReactReduxContext, - ), + connectOptions, )(dartComponentClass); /// Use a Dart proxy instead of a JS one since we're treating it like a Dart component: @@ -263,9 +270,6 @@ UiFactory Function(UiFactory) connect a == b; -bool _shallowMapEquality(Map a, Map b) => const MapEquality().equals(a, b); - @JS('ReactRedux.connect') external ReactClass Function(ReactClass) _jsConnect( [ @@ -367,10 +371,15 @@ class ReactJsReactReduxComponentFactoryProxy extends ReactJsContextComponentFact } /// Converts a Redux.dart [Store] into a Javascript object formatted for consumption by react-redux. -JsReactReduxStore _reduxifyStore(Store store){ +JsReactReduxStore _reduxifyStore(Store store) { + // Memoize this so that the same ReactInteropValue instances will be used + // for a given state, allowing JS `===` checks to not fail when the same + // state object is passed. + final memoizedWrapInteropValue = imemo1(wrapInteropValue); + return JsReactReduxStore( getState: allowInterop(() { - return wrapInteropValue(store.state); + return memoizedWrapInteropValue(store.state); }), subscribe: allowInterop((cb) { return allowInterop(store.onChange.listen((_){cb();}).cancel); diff --git a/lib/src/util/equality.dart b/lib/src/util/equality.dart new file mode 100644 index 000000000..a6db0570b --- /dev/null +++ b/lib/src/util/equality.dart @@ -0,0 +1,35 @@ +/// Returns whether maps [a] and [b] have [identical] sets of values for the same keys. +// +// Ported from https://github.com/reduxjs/react-redux/blob/573db0bfc8d1d50fdb6e2a98bd8a7d4675fecf11/src/utils/shallowEqual.js +// +// The MIT License (MIT) +// +// Copyright (c) 2015-present Dan Abramov +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +bool areMapsShallowIdentical(Map a, Map b) { + if (identical(a, b)) return true; + if (a.length != b.length) return false; + for (final key in a.keys) { + if (!b.containsKey(key)) return false; + if (!identical(b[key], a[key])) return false; + } + return true; +} diff --git a/pubspec.yaml b/pubspec.yaml index 5f9ce1eeb..ed0d58048 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: dart_style: ^1.2.5 js: ^0.6.1+1 logging: ">=0.11.3+2 <1.0.0" + memoize: ^2.0.0 meta: ^1.1.6 path: ^1.5.1 react: ^5.3.0 diff --git a/test/over_react/component_declaration/component_base_test.dart b/test/over_react/component_declaration/component_base_test.dart index cf4bc4843..facb9863f 100644 --- a/test/over_react/component_declaration/component_base_test.dart +++ b/test/over_react/component_declaration/component_base_test.dart @@ -86,19 +86,35 @@ main() { }); } - void _commonVariadicChildrenTests(UiProps builder) { + void _commonVariadicChildrenTests(UiProps builder, {bool alwaysExpectList = false}) { // There are different code paths for 0, 1, 2, 3, 4, 5, 6, and 6+ arguments. // Test all of them. group('a number of variadic children:', () { - test('0', () { - final instance = builder(); - expect(getJsChildren(instance), isNull); - }); + if (alwaysExpectList) { + test('0', () { + final instance = builder(); + expect(getJsChildren(instance), []); - test('1', () { - final instance = builder(1); - expect(getJsChildren(instance), equals(1)); - }); + final instance2 = builder(); + expect(getJsChildren(instance2), same(getJsChildren(instance)), + reason: 'zero arg children should always be the same List instance for perf reasons'); + }); + + test('1', () { + final instance = builder(1); + expect(getJsChildren(instance), [1]); + }); + } else { + test('0', () { + final instance = builder(); + expect(getJsChildren(instance), isNull); + }); + + test('1', () { + final instance = builder(1); + expect(getJsChildren(instance), equals(1)); + }); + } const firstGeneralCaseVariadicChildCount = 2; const maxSupportedVariadicChildCount = 40; @@ -137,9 +153,22 @@ main() { stopRecordingValidationWarnings(); }); - group('renders a DOM component with the correct children when', () { - _commonVariadicChildrenTests(Dom.div()); + group('creates a ReactElement with the correct children:', () { + group('DomProps:', () { + _commonVariadicChildrenTests(Dom.div()); + }); + // Need to test these separately because they have different JS factory proxies + group('Dart Component:', () { + _commonVariadicChildrenTests(TestComponent()); + }); + + group('Dart Component2:', () { + _commonVariadicChildrenTests(TestComponent2(), alwaysExpectList: true); + }); + }); + + group('renders a DOM component with the correct children when', () { test('no children are passed in', () { var renderedNode = renderAndGetDom(Dom.div()()); @@ -201,8 +230,6 @@ main() { }, tags: 'ddc'); group('renders a composite Dart component with the correct children when', () { - _commonVariadicChildrenTests(TestComponent()); - test('no children are passed in', () { var renderedInstance = render(TestComponent()()); diff --git a/test/over_react/util/equality_test.dart b/test/over_react/util/equality_test.dart new file mode 100644 index 000000000..131c07485 --- /dev/null +++ b/test/over_react/util/equality_test.dart @@ -0,0 +1,163 @@ +@TestOn('browser || vm') +import 'package:over_react/src/util/equality.dart'; +import 'package:test/test.dart'; + +main() { + group('areMapsShallowIdentical', () { + /// Expect that `areMapsShallowIdentical(a, b)` and `areMapsShallowIdentical(b, a)` + /// have the same result (represented by [matcher]). + void expectAreMapsShallowIdenticalCommutatively(Map a, Map b, matcher) { + final abResult = areMapsShallowIdentical(a, b); + final baResult = areMapsShallowIdentical(b, a); + expect([abResult, baResult], everyElement(matcher)); + } + + group('returns true for', () { + test('different maps that are equal', () { + final a = { + 1: 'one', + 2: 'two', + }; + final b = { + 1: 'one', + 2: 'two', + }; + expectAreMapsShallowIdenticalCommutatively(a, b, isTrue); + }); + + test('different maps that are equal with keys in different orders', () { + final a = { + 1: 'one', + 2: 'two', + }; + final b = { + 2: 'two', + 1: 'one', + }; + expectAreMapsShallowIdenticalCommutatively(a, b, isTrue); + }); + + test('different empty maps', () { + final a = {}; + final b = {}; + expectAreMapsShallowIdenticalCommutatively(a, b, isTrue); + }); + + test('the same map instance', () { + final a = {1: 'one', 2: 'two'}; + final b = a; + expectAreMapsShallowIdenticalCommutatively(a, b, isTrue); + }); + }); + + group('returns true for', () { + test('different maps that are equal', () { + final a = { + 1: 'one', + 2: 'two', + }; + final b = { + 2: 'two', + 1: 'one', + }; + expectAreMapsShallowIdenticalCommutatively(a, b, isTrue); + }); + + test('different empty maps', () { + final a = {}; + final b = {}; + expectAreMapsShallowIdenticalCommutatively(a, b, isTrue); + }); + + test('the same map instance', () { + final a = {1: 'one', 2: 'two'}; + final b = a; + expectAreMapsShallowIdenticalCommutatively(a, b, isTrue); + }); + }); + + group('returns false for', () { + test('maps of different length', () { + final a = { + 1: 'one', + }; + final b = { + 1: 'one', + 2: 'two', + }; + expectAreMapsShallowIdenticalCommutatively(a, b, isFalse); + }); + + test('maps with some differing keys and a common set of equal pairs', () { + final a = { + 1: 'one', + 2: 'two', + }; + final b = { + 2: 'two', + 3: 'three', + }; + expectAreMapsShallowIdenticalCommutatively(a, b, isFalse); + }); + + test('maps with different keys but equal values', () { + final a = { + 1: 'foo', + 2: 'bar', + }; + final b = { + 3: 'foo', + 4: 'bar', + }; + expectAreMapsShallowIdenticalCommutatively(a, b, isFalse); + }); + + test('maps with different values but equal keys', () { + final a = { + 1: 'one', + 2: 'two', + }; + final b = { + 1: 'one', + 2: 'not two', + }; + expectAreMapsShallowIdenticalCommutatively(a, b, isFalse); + }); + + test('maps with equal but not identical values', () { + final valueA = EqualHelper('foo'); + final valueB = EqualHelper('foo'); + expect(valueA, allOf(equals(valueB), isNot(same(valueB))), + reason: 'test setup check'); + + final a = {1: valueA}; + final b = {1: valueB}; + expectAreMapsShallowIdenticalCommutatively(a, b, isFalse); + }); + }); + + test('gracefully handles maps of different types', () { + final a = {'1': 1}; + final b = {true: 'true'}; + expect(b, hasLength(a.length), + reason: 'test setup check; we want the to get further than' + ' the length short circuit in the function'); + + expect(() => areMapsShallowIdentical(a, b), returnsNormally); + expectAreMapsShallowIdenticalCommutatively(a, b, isFalse); + }); + }); +} + +class EqualHelper { + final String value; + + EqualHelper(this.value); + + @override + int get hashCode => value.hashCode; + + @override + bool operator ==(Object other) => + other is EqualHelper && other.value == value; +} diff --git a/test/over_react/util/hoc_test.dart b/test/over_react/util/hoc_test.dart index 3b989a985..355378510 100644 --- a/test/over_react/util/hoc_test.dart +++ b/test/over_react/util/hoc_test.dart @@ -14,6 +14,7 @@ import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; +import 'package:redux/redux.dart'; import 'package:test/test.dart'; import '../../over_react_redux/fixtures/counter.dart'; @@ -22,6 +23,12 @@ import '../../test_util/test_util.dart'; main() { group('composeHocs', () { + Store store1; + + setUp(() { + store1 = Store(counterStateReducer, initialState: CounterState()); + }); + test('will render a nested component', () { CounterProps Function() components = composeHocs([ connect(), diff --git a/test/over_react_redux/connect_flux_integration_test.dart b/test/over_react_redux/connect_flux_integration_test.dart index 0d7f318b8..369ad92f6 100644 --- a/test/over_react_redux/connect_flux_integration_test.dart +++ b/test/over_react_redux/connect_flux_integration_test.dart @@ -28,13 +28,26 @@ import 'fixtures/flux_counter.dart'; main() { group('connectFlux integration -', () { - setUp(() async { - // Reset stores state to initialState value - store1.dispatch(ResetAction()); - store2.dispatch(ResetAction()); - - // wait for state to update - await Future(() {}); + FluxActions fluxActions; + FluxStore fluxStore; + FluxToReduxAdapterStore store1; + + FluxActions connectableStoreActions; + TestConnectableFluxStore connectableFluxStore; + TestConnectableFluxStore anotherConnectableFluxStore; + ConnectFluxAdapterStore connectableFluxAdaptedStore; + + setUp(() { + fluxActions = FluxActions(); + fluxStore = FluxStore(fluxActions); + store1 = FluxToReduxAdapterStore(fluxStore, fluxActions); + + connectableStoreActions = FluxActions(); + connectableFluxStore = TestConnectableFluxStore(connectableStoreActions); + anotherConnectableFluxStore = + TestConnectableFluxStore(connectableStoreActions); + connectableFluxAdaptedStore = + ConnectFluxAdapterStore(connectableFluxStore, connectableStoreActions); }); group('FluxToReduxAdapterStore', () { @@ -44,6 +57,8 @@ main() { connectFlux( mapStateToProps: (state) => (ConnectFluxCounter()..currentCount = state.count), + mapActionsToProps: (actions) => + (ConnectFluxCounter()..actions = actions), )(ConnectFluxCounter); final ConnectedReduxComponent = connect( @@ -257,11 +272,6 @@ main() { verifyCount(containerList, 0); }); - tearDown(() { - connectableStoreActions.resetAction(); - connectableFluxStore.trigger(); - }); - test('will keep Flux, Redux, and connectFlux components all in sync', () async { final fluxButton = queryByTestId(fluxCounter, 'button-increment'); diff --git a/test/over_react_redux/connect_flux_test.dart b/test/over_react_redux/connect_flux_test.dart index 245d82dd1..2a8cfac43 100644 --- a/test/over_react_redux/connect_flux_test.dart +++ b/test/over_react_redux/connect_flux_test.dart @@ -15,6 +15,7 @@ import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_flux.dart'; import 'package:over_react/over_react_redux.dart'; +import 'package:redux/redux.dart' as redux; import 'package:test/test.dart'; import '../test_util/test_util.dart'; @@ -33,6 +34,14 @@ main() { TestJacket jacket; final counterRef = createRef(); + FluxActions fluxActions; + FluxStore fluxStore; + FluxToReduxAdapterStore store1; + + FluxActions bigFluxActions; + FluxStore2 bigFluxCounter; + FluxToReduxAdapterStore store2; + JsConnectOptions connectOptions; final originalConnect = mockableJsConnect; @@ -53,15 +62,17 @@ main() { mockableJsConnect = originalConnect; }); - setUp(() async { + setUp(() { ConnectedCounter = null; jacket = null; - // Reset stores state to initialState value - store1.dispatch(ResetAction()); - store2.dispatch(ResetAction()); - // wait for state to update - await Future(() {}); + fluxActions = FluxActions(); + fluxStore = FluxStore(fluxActions); + store1 = FluxToReduxAdapterStore(fluxStore, fluxActions); + + bigFluxActions = FluxActions(); + bigFluxCounter = FluxStore2(bigFluxActions); + store2 = FluxToReduxAdapterStore(bigFluxCounter, bigFluxActions); }); group('behaves like redux with', () { @@ -379,17 +390,15 @@ main() { group('areStatePropsEqual', () { List> methodsCalled; - const mountMethodCalls = [ + const expectedMountMethodCalls = [ 'mapStateToProps', 'mapStateToProps', 'areStatePropsEqual', ]; - const updateMethodCalls = [ + const expectedUpdateMethodCalls = [ 'mapStateToProps', 'areStatePropsEqual', - 'mapStateToProps', - 'areStatePropsEqual' ]; setUp(() { @@ -400,7 +409,7 @@ main() { ConnectedCounter = connectFlux( mapStateToProps: (state) { - methodsCalled.add({'called': 'mapStateToProps'}); + methodsCalled.add({'called': 'mapStateToProps', 'state': state}); return ConnectFluxCounter()..currentCount = state.count; }, mapActionsToProps: (actions) => @@ -419,16 +428,16 @@ main() { jacket = mount( (ReduxProvider()..store = store1)( - (ConnectedCounter() - ..ref = counterRef - ..currentCount = 0 - )('test'), + (ConnectedCounter()..ref = counterRef)('test'), ), ); // Because `areStatesEqual` is false, we expect additional method calls - expect(methodsCalled.map((methodObj) => methodObj['called']), - mountMethodCalls); + expect( + methodsCalled, + expectedMountMethodCalls + .map((expected) => containsPair('called', expected)) + .toList()); for (final methodCall in methodsCalled) { if (methodCall['called'] == 'areStatePropsEqual') { expect(methodCall['prev'], isA()); @@ -446,8 +455,11 @@ main() { await Future(() {}); // store.state.count should be 1 but does not re-render due to override in `areStatePropsEqual` - expect(methodsCalled.map((methodObj) => methodObj['called']), - updateMethodCalls); + expect( + methodsCalled, + expectedUpdateMethodCalls + .map((expected) => containsPair('called', expected)) + .toList()); for (final methodCall in methodsCalled) { if (methodCall['called'] == 'areStatePropsEqual') { expect(methodCall['prev'], isA()); @@ -459,15 +471,18 @@ main() { }); test( - 'matches a standard Redux component when `areStatesEqual` is false', + 'matches a Redux component with impure state when `areStatesEqual` is false', () async { final localReduxRef = createRef(); final ReduxConnectedCounter = - connect( + connect( mapStateToProps: (state) { - methodsCalled.add({'called': 'mapStateToProps'}); - return ConnectFluxCounter()..currentCount = state.count; + methodsCalled.add({ + 'called': 'mapStateToProps', + 'state': state, + }); + return Counter()..currentCount = state.count; }, areStatePropsEqual: (next, prev) { methodsCalled.add({ @@ -482,18 +497,28 @@ main() { areStatesEqual: (_, __) => false, )(Counter); + // In this setup with an idiomatic redux store, we'd expect double the updates when the state is updated + // since we're dealing with a new state object instance every time. + // + // However, in Flux, it's the same, identical state object (the Flux store) every time + // which allows react-redux's memoization to skip the extra calls after the component renders. + // + // Simulate this by using an Redux store that has the same impurity as Flux stores. + final impureReduxStore = redux.Store(redux_store.impureCounterStateReducer, + initialState: redux_store.ImpureCounterState()); + jacket = mount( - (ReduxProvider()..store = redux_store.store1)( - (ReduxConnectedCounter() - ..ref = localReduxRef - ..currentCount = 0 - )('test'), + (ReduxProvider()..store = impureReduxStore)( + (ReduxConnectedCounter()..ref = localReduxRef)('test'), ), ); // Because `areStatesEqual` is false, we expect additional method calls - expect(methodsCalled.map((methodObj) => methodObj['called']), - mountMethodCalls); + expect( + methodsCalled, + expectedMountMethodCalls + .map((expected) => containsPair('called', expected)) + .toList()); for (final methodCall in methodsCalled) { if (methodCall['called'] == 'areStatePropsEqual') { expect(methodCall['prev'], isA()); @@ -510,8 +535,14 @@ main() { await Future(() {}); // store.state.count should be 1 but does not re-render due to override in `areStatePropsEqual` - expect(methodsCalled.map((methodObj) => methodObj['called']), - updateMethodCalls); + expect( + methodsCalled, + expectedUpdateMethodCalls + .map((expected) => containsPair('called', expected)) + .toList(), + reason: + 'connect\'s sequence of calls should match expectedUpdateMethodCalls'); + for (final methodCall in methodsCalled) { if (methodCall['called'] == 'areStatePropsEqual') { expect(methodCall['prev'], isA()); @@ -691,7 +722,53 @@ main() { }, }; - testParameterCases(testCases); + testCases.forEach((parameterCase, parameters) { + bool shouldDomUpdate(Map parameters) => + (parameters['mapStateToProps'] != null || + parameters['mapStateToPropsWithOwnProps'] != null); + + test(parameterCase, () async { + final ConnectedFluxComponent = + connectFlux( + mapStateToProps: parameters['mapStateToProps'], + mapActionsToProps: parameters['mapActionsToProps'], + mapStateToPropsWithOwnProps: + parameters['mapStateToPropsWithOwnProps'], + mapActionsToPropsWithOwnProps: + parameters['mapActionsToPropsWithOwnProps'], + )(ConnectFluxCounter); + + final jacket2 = mount((ReduxProvider()..store = store1)( + (ConnectedFluxComponent() + ..actions = fluxActions + ..addTestId('flux-component') + )(), + )); + + final fluxCounter = + queryByTestId(jacket2.mountNode, 'flux-component'); + final fluxButton = queryByTestId(fluxCounter, 'button-increment'); + + expect(fluxStore.state.count, 0); + + click(fluxButton); + await Future(() {}); + + expect(fluxStore.state.count, 1); + + if (shouldDomUpdate(parameters)) { + expect(findDomNode(fluxCounter).innerHtml, contains('Count: 1')); + } + + store1.dispatch(ResetAction()); + await Future(() {}); + + expect(fluxStore.state.count, 0); + if (shouldDomUpdate(parameters)) { + expect(findDomNode(fluxCounter).innerHtml, contains('Count: 0')); + } + }); + }); }); test('prints a warning when state is mutated directly', () async { @@ -739,49 +816,3 @@ mapStateToPropsWithOwnProps get testMapStateToPropsWithOwnProps => mapActionsToPropsWithOwnProps get testMapActionsToPropsWithOwnProps => (actions, ownProps) => (ConnectFluxCounter()..increment = actions.incrementAction); - -void testParameterCases(Map cases) { - for (final parameterCase in cases.keys) { - final parameters = cases[parameterCase]; - bool shouldDomUpdate(Map parameters) => - (parameters['mapStateToProps'] != null || - parameters['mapStateToPropsWithOwnProps'] != null); - - test(parameterCase, () async { - final ConnectedFluxComponent = - connectFlux( - mapStateToProps: parameters['mapStateToProps'], - mapActionsToProps: parameters['mapActionsToProps'], - mapStateToPropsWithOwnProps: parameters['mapStateToPropsWithOwnProps'], - mapActionsToPropsWithOwnProps: - parameters['mapActionsToPropsWithOwnProps'], - )(ConnectFluxCounter); - - final jacket = mount((ReduxProvider()..store = store1)( - (ConnectedFluxComponent()..addTestId('flux-component'))(), - )); - - final fluxCounter = queryByTestId(jacket.mountNode, 'flux-component'); - final fluxButton = queryByTestId(fluxCounter, 'button-increment'); - - expect(fluxStore.state.count, 0); - - click(fluxButton); - await Future(() {}); - - expect(fluxStore.state.count, 1); - - if (shouldDomUpdate(parameters)) { - expect(findDomNode(fluxCounter).innerHtml, contains('Count: 1')); - } - - store1.dispatch(ResetAction()); - await Future(() {}); - - expect(fluxStore.state.count, 0); - if (shouldDomUpdate(parameters)) { - expect(findDomNode(fluxCounter).innerHtml, contains('Count: 0')); - } - }); - } -} diff --git a/test/over_react_redux/connect_test.dart b/test/over_react_redux/connect_test.dart index 2beacb4ff..38e371e8a 100644 --- a/test/over_react_redux/connect_test.dart +++ b/test/over_react_redux/connect_test.dart @@ -16,6 +16,7 @@ library abstract_transition_test; import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; +import 'package:redux/redux.dart'; import 'package:test/test.dart'; import '../test_util/test_util.dart'; @@ -32,6 +33,8 @@ main() { UiFactory ConnectedCounter; TestJacket jacket; final counterRef = createRef(); + Store store1; + Store store2; JsConnectOptions connectOptions; final originalConnect = mockableJsConnect; @@ -53,15 +56,12 @@ main() { mockableJsConnect = originalConnect; }); - setUp(() async { + setUp(() { ConnectedCounter = null; jacket = null; - // Reset stores state to initalState value. - store1.dispatch(ResetAction()); - store2.dispatch(ResetAction()); - // wait for state to update - await Future(() {}); + store1 = Store(counterStateReducer, initialState: CounterState()); + store2 = Store(bigCounterStateReducer, initialState: BigCounterState()); }); test('throws when mounting a UiComponent', () { @@ -248,10 +248,7 @@ main() { group('mapDispatchToProps', () { test('maps dispatcher to props correctly', () async { ConnectedCounter = connect( - mapStateToProps: (state) { - expect(state, isA()); - return Counter()..currentCount = state.count; - }, + mapStateToProps: (state) => (Counter()..currentCount = state.count), mapDispatchToProps: (dispatch) { return Counter()..decrement = () => dispatch(DecrementAction()); }, @@ -285,10 +282,7 @@ main() { group('mapDispatchToPropsWithOwnProps', () { test('maps dispatcher to props correctly', () async { ConnectedCounter = connect( - mapStateToProps: (state) { - expect(state, isA()); - return Counter()..currentCount = state.count; - }, + mapStateToProps: (state) => (Counter()..currentCount = state.count), mapDispatchToPropsWithOwnProps: (dispatch, ownProps) { return Counter()..decrement = () => dispatch(DecrementAction()); }, @@ -329,9 +323,6 @@ main() { return Counter()..decrement = () => dispatch(DecrementAction()); }, mergeProps: (stateProps, dispatchProps, ownProps) { - expect(stateProps, isA()); - expect(dispatchProps, isA()); - expect(ownProps, isA()); return Counter() // Return whatever value is passed through ownProps until the state count is over 1 ..currentCount = stateProps.currentCount < 1 @@ -398,16 +389,14 @@ main() { group('areStatePropsEqual', () { test('', () async { - List methodsCalled = []; + final calls = >[]; ConnectedCounter = connect( mapStateToProps: (state) { - methodsCalled.add('mapStateToProps'); + calls.add({'name': 'mapStateToProps'}); return Counter()..currentCount = state.count; }, areStatePropsEqual: (next, prev) { - expect(next, isA()); - expect(prev, isA()); - methodsCalled.add('areStatePropsEqual'); + calls.add({'name': 'areStatePropsEqual', 'next': next, 'prev': prev}); // Force it to always be true, meaing it shouldnt re-render if they change. return true; }, @@ -422,8 +411,10 @@ main() { )('test'), ), ); - expect(methodsCalled, ['mapStateToProps']); - methodsCalled.clear(); + expect(calls, [ + {'name': 'mapStateToProps'}, + ]); + calls.clear(); var dispatchButton = queryByTestId(jacket.mountNode, 'button-increment'); @@ -434,7 +425,14 @@ main() { // store.state.count should be 1 but does not re-render due to override in `areStatePropsEqual` - expect(methodsCalled, ['mapStateToProps', 'areStatePropsEqual']); + expect(calls, [ + {'name': 'mapStateToProps'}, + { + 'name': 'areStatePropsEqual', + 'next': isA(), + 'prev': isA(), + } + ]); expect(jacket.mountNode.innerHtml, contains('Count: 0')); }); }); @@ -462,16 +460,20 @@ main() { group('areStatesEqual', () { test('', () async { - List methodsCalled = []; + final calls = >[]; + getCallNames() => calls.map((call) => call['name']).toList(); + ConnectedCounter = connect( areStatesEqual: (next, prev) { - expect(next, isA()); - expect(prev, isA()); - methodsCalled.add('areStatesEqual'); + calls.add({ + 'name': 'areStatesEqual', + 'prev': prev, + 'next': next, + }); return true; }, mapStateToProps: (state) { - methodsCalled.add('mapStateToProps'); + calls.add({'name': 'mapStateToProps'}); return Counter()..currentCount = state.count; }, forwardRef: true, @@ -479,16 +481,21 @@ main() { jacket = mount( (ReduxProvider()..store = store1)( - (ConnectedCounter() - ..ref = counterRef - ..currentCount = 0 - )('test'), + (ConnectedCounter()..ref = counterRef)('test'), ), ); - expect(methodsCalled, contains('mapStateToProps')); - expect(methodsCalled, contains('areStatesEqual')); - methodsCalled.clear(); + expect(getCallNames(), + containsAll({'areStatesEqual', 'mapStateToProps'})); + expect( + calls.firstWhere((call) => call['name'] == 'areStatesEqual'), + { + 'name': 'areStatesEqual', + 'prev': isA(), + 'next': isA(), + }, + reason: 'states should be passed in as arguments'); + calls.clear(); var dispatchButton = queryByTestId(jacket.mountNode, 'button-increment'); @@ -498,10 +505,46 @@ main() { await Future(() {}); // only calls `areStatesEqual` and does not call `mapStateToProps` since it returned `true`. - expect(methodsCalled, isNot(contains('mapStateToProps'))); - expect(methodsCalled, contains('areStatesEqual')); + expect(getCallNames(), isNot(contains('mapStateToProps'))); + expect(getCallNames(), contains('areStatesEqual')); expect(jacket.mountNode.innerHtml, contains('Count: 0')); }); + + group('when not specified,', () { + // This indirectly tests the memoization of wrapInteropValue + test('does not rerender when the state object is identical', () async { + List methodsCalled = []; + ConnectedCounter = connect( + mapStateToProps: (state) { + methodsCalled.add('mapStateToProps'); + return Counter()..currentCount = state.count; + }, + forwardRef: true, + )(Counter); + + final noopStore = Store((state, action) => state, initialState: CounterState(count: 0)); + + jacket = mount( + (ReduxProvider()..store = noopStore)( + (ConnectedCounter()..ref = counterRef..currentCount = 0)('test'), + ), + ); + + expect(methodsCalled, contains('mapStateToProps')); + methodsCalled.clear(); + + final stateBefore = noopStore.state; + noopStore.dispatch('Dummy action that does not mutate state, but triggers connect'); + // wait for the next tick for the async dispatch to propagate + await Future(() {}); + final stateAfter = noopStore.state; + expect(stateAfter, same(stateBefore), + reason: 'test setup check; store should have the exact same state object'); + + expect(methodsCalled, isEmpty, + reason: 'mapStateToProps should not be called since states are equal according to connect'); + }); + }); }); }); diff --git a/test/over_react_redux/fixtures/connect_flux_counter.dart b/test/over_react_redux/fixtures/connect_flux_counter.dart index e621fe3d2..918486453 100644 --- a/test/over_react_redux/fixtures/connect_flux_counter.dart +++ b/test/over_react_redux/fixtures/connect_flux_counter.dart @@ -34,6 +34,8 @@ class _$ConnectFluxCounterProps extends UiProps { void Function() decrement; void Function() mutateStoreDirectly; + + FluxActions actions; } @Component2() @@ -54,7 +56,7 @@ class ConnectFluxCounterComponent } else if (props.increment != null) { props.increment(); } else { - fluxActions.incrementAction(); + props.actions.incrementAction(); } } )('+'), diff --git a/test/over_react_redux/fixtures/connect_flux_counter.over_react.g.dart b/test/over_react_redux/fixtures/connect_flux_counter.over_react.g.dart index 675ee4bdb..229f0406e 100644 --- a/test/over_react_redux/fixtures/connect_flux_counter.over_react.g.dart +++ b/test/over_react_redux/fixtures/connect_flux_counter.over_react.g.dart @@ -83,6 +83,16 @@ abstract class _$ConnectFluxCounterPropsAccessorsMixin @override set mutateStoreDirectly(void Function() value) => props[_$key__mutateStoreDirectly___$ConnectFluxCounterProps] = value; + + /// + @override + FluxActions get actions => + props[_$key__actions___$ConnectFluxCounterProps] ?? + null; // Add ` ?? null` to workaround DDC bug: ; + /// + @override + set actions(FluxActions value) => + props[_$key__actions___$ConnectFluxCounterProps] = value; /* GENERATED CONSTANTS */ static const PropDescriptor _$prop__currentCount___$ConnectFluxCounterProps = PropDescriptor(_$key__currentCount___$ConnectFluxCounterProps); @@ -97,6 +107,8 @@ abstract class _$ConnectFluxCounterPropsAccessorsMixin static const PropDescriptor _$prop__mutateStoreDirectly___$ConnectFluxCounterProps = PropDescriptor(_$key__mutateStoreDirectly___$ConnectFluxCounterProps); + static const PropDescriptor _$prop__actions___$ConnectFluxCounterProps = + PropDescriptor(_$key__actions___$ConnectFluxCounterProps); static const String _$key__currentCount___$ConnectFluxCounterProps = 'ConnectFluxCounterProps.currentCount'; static const String _$key__wrapperStyles___$ConnectFluxCounterProps = @@ -109,6 +121,8 @@ abstract class _$ConnectFluxCounterPropsAccessorsMixin 'ConnectFluxCounterProps.decrement'; static const String _$key__mutateStoreDirectly___$ConnectFluxCounterProps = 'ConnectFluxCounterProps.mutateStoreDirectly'; + static const String _$key__actions___$ConnectFluxCounterProps = + 'ConnectFluxCounterProps.actions'; static const List $props = [ _$prop__currentCount___$ConnectFluxCounterProps, @@ -116,7 +130,8 @@ abstract class _$ConnectFluxCounterPropsAccessorsMixin _$prop__mutatedList___$ConnectFluxCounterProps, _$prop__increment___$ConnectFluxCounterProps, _$prop__decrement___$ConnectFluxCounterProps, - _$prop__mutateStoreDirectly___$ConnectFluxCounterProps + _$prop__mutateStoreDirectly___$ConnectFluxCounterProps, + _$prop__actions___$ConnectFluxCounterProps ]; static const List $propKeys = [ _$key__currentCount___$ConnectFluxCounterProps, @@ -124,7 +139,8 @@ abstract class _$ConnectFluxCounterPropsAccessorsMixin _$key__mutatedList___$ConnectFluxCounterProps, _$key__increment___$ConnectFluxCounterProps, _$key__decrement___$ConnectFluxCounterProps, - _$key__mutateStoreDirectly___$ConnectFluxCounterProps + _$key__mutateStoreDirectly___$ConnectFluxCounterProps, + _$key__actions___$ConnectFluxCounterProps ]; } diff --git a/test/over_react_redux/fixtures/connect_flux_store.dart b/test/over_react_redux/fixtures/connect_flux_store.dart index 14b138f62..17f535a62 100644 --- a/test/over_react_redux/fixtures/connect_flux_store.dart +++ b/test/over_react_redux/fixtures/connect_flux_store.dart @@ -18,10 +18,8 @@ import 'package:over_react/over_react_flux.dart'; import 'redux_actions.dart'; -int initialValue = 0; - int _resetCounterReducer(int currentCount, ResetAction action) { - return initialValue; + return 0; } class FluxActions { @@ -43,7 +41,7 @@ class FluxStore extends flux.Store with InfluxStoreMixin { state.listThatYouDefShouldntMutate; FluxStore(this._actions) { - state = FluxCounterState(count: initialValue); + state = FluxCounterState(count: 0); triggerOnActionV2( _actions.incrementAction, (_) => this.influxReducer(IncrementAction())); @@ -74,7 +72,7 @@ FluxCounterState counterStateReducer(FluxCounterState state, Object action) { } else if (action is DecrementAction) { return FluxCounterState(count: state.count - (action?.value ?? 1)); } else if (action is ResetAction) { - return FluxCounterState(count: initialValue); + return FluxCounterState(count: 0); } else if (action is MutateStoreDirectlyAction) { state.listThatYouDefShouldntMutate.add('woops'); } @@ -82,10 +80,10 @@ FluxCounterState counterStateReducer(FluxCounterState state, Object action) { return state; } -FluxActions fluxActions = FluxActions(); -FluxStore fluxStore = FluxStore(fluxActions); -FluxToReduxAdapterStore store1 = - FluxToReduxAdapterStore(fluxStore, fluxActions); +// To use in tests, copy-paste: +// var fluxActions = FluxActions(); +// var fluxStore = FluxStore(fluxActions); +// var store1 = FluxToReduxAdapterStore(fluxStore, fluxActions); /////////////////////////////// STORE 2 "BigCounter" /////////////////////////////// class FluxStore2 extends flux.Store with InfluxStoreMixin { @@ -98,7 +96,7 @@ class FluxStore2 extends flux.Store with InfluxStoreMixin { String get name => state.name; FluxStore2(this._actions) { - state = BigCounterState(bigCount: initialValue); + state = BigCounterState(bigCount: 0); triggerOnActionV2(_actions.incrementAction, (count) => this.influxReducer(IncrementAction(count))); @@ -137,10 +135,10 @@ BigCounterState bigCounterStateReducer(BigCounterState state, action) => bigCount: bigCounterActionsReducer(state.bigCount, action), ); -FluxActions bigFluxActions = FluxActions(); -FluxStore2 bigFluxCounter = FluxStore2(bigFluxActions); -FluxToReduxAdapterStore store2 = - FluxToReduxAdapterStore(bigFluxCounter, bigFluxActions); +// To use in tests, copy-paste: +// final bigFluxActions = FluxActions(); +// final bigFluxCounter = FluxStore2(bigFluxActions); +// final store2 = FluxToReduxAdapterStore(bigFluxCounter, bigFluxActions); /////////////////////////////// STORE 3 "AnotherFluxStore" /////////////////////////////// // Just created for testing context @@ -178,10 +176,8 @@ class TestConnectableFluxStore extends flux.Store { } } -FluxActions connectableStoreActions = FluxActions(); -TestConnectableFluxStore connectableFluxStore = - TestConnectableFluxStore(connectableStoreActions); -TestConnectableFluxStore anotherConnectableFluxStore = - TestConnectableFluxStore(connectableStoreActions); -ConnectFluxAdapterStore connectableFluxAdaptedStore = - ConnectFluxAdapterStore(connectableFluxStore, connectableStoreActions); +// To use in tests, copy-paste +// final connectableStoreActions = FluxActions(); +// final connectableFluxStore = TestConnectableFluxStore(connectableStoreActions); +// final anotherConnectableFluxStore = TestConnectableFluxStore(connectableStoreActions); +// final connectableFluxAdaptedStore = ConnectFluxAdapterStore(connectableFluxStore, connectableStoreActions); diff --git a/test/over_react_redux/fixtures/store.dart b/test/over_react_redux/fixtures/store.dart index d4acb9ee0..1d1358bc3 100644 --- a/test/over_react_redux/fixtures/store.dart +++ b/test/over_react_redux/fixtures/store.dart @@ -16,22 +16,27 @@ import 'package:redux/redux.dart'; import 'redux_actions.dart'; -int initialValue = 0; - int _resetCounterReducer(int currentCount, ResetAction action){ - return initialValue; + return 0; } /////////////////////////////// STORE 1 "Counter" /////////////////////////////// -Store store1 = Store(counterStateReducer, initialState: CounterState(count: initialValue)); +// To use in tests, copy-paste: +// Store store1 = Store(counterStateReducer, initialState: CounterState(count: initialValue)); class CounterState { final int count; final String name; CounterState({ - this.count, + this.count = 0, this.name = 'Counter', }); + + @override + toString() => 'CounterState:${{ + 'count': count, + 'name': name, + }}'; } int _counterDecrementReducer(int currentCount, DecrementAction action) { @@ -53,13 +58,14 @@ CounterState counterStateReducer(CounterState state, action) => CounterState( ); /////////////////////////////// STORE 2 "BigCounter" /////////////////////////////// -Store store2 = Store(bigCounterStateReducer, initialState: BigCounterState(bigCount: initialValue)); +// To use in tests, copy-paste: +// Store store2 = Store(bigCounterStateReducer, initialState: BigCounterState(bigCount: initialValue)); class BigCounterState { final int bigCount; final String name; BigCounterState({ - this.bigCount, + this.bigCount = 0, this.name = 'BigCounter', }); } @@ -82,3 +88,25 @@ BigCounterState bigCounterStateReducer(BigCounterState state, action) => BigCoun bigCount: bigCounterActionsReducer(state.bigCount, action), ); +/////////////////////////////// "ImpureCounter" /////////////////////////////// + +class ImpureCounterState { + int count; + String name; + + ImpureCounterState({ + this.count = 0, + this.name = 'Counter', + }); + + @override + toString() => 'CounterState:${{ + 'count': count, + 'name': name, + }}'; +} + +ImpureCounterState impureCounterStateReducer( + ImpureCounterState state, action) => + // This is the impure part: modify the state directly + state..count = counterActionsReducer(state.count, action); diff --git a/test/over_react_redux/redux_multi_provider_test.dart b/test/over_react_redux/redux_multi_provider_test.dart index 0f8b401ac..939a5a59c 100644 --- a/test/over_react_redux/redux_multi_provider_test.dart +++ b/test/over_react_redux/redux_multi_provider_test.dart @@ -34,6 +34,18 @@ main() { group('ReduxMultiProvider', () { test('creates a provider for every store', () async { + final fluxActions = FluxActions(); + final fluxStore = FluxStore(fluxActions); + final store1 = FluxToReduxAdapterStore(fluxStore, fluxActions); + + final bigFluxActions = FluxActions(); + final bigFluxCounter = FluxStore2(bigFluxActions); + final store2 = FluxToReduxAdapterStore(bigFluxCounter, bigFluxActions); + + final anotherFluxActionsInstance = FluxActions(); + final anotherFluxStore = FluxStore(anotherFluxActionsInstance); + final store3 = FluxToReduxAdapterStore(anotherFluxStore, anotherFluxActionsInstance); + final Context1ConnectedFluxComponent = connectFlux( mapStateToProps: (state) => diff --git a/test/over_react_util_test.dart b/test/over_react_util_test.dart index 646d73e72..383f83638 100644 --- a/test/over_react_util_test.dart +++ b/test/over_react_util_test.dart @@ -28,6 +28,7 @@ import 'over_react/util/constants_base_test.dart' as constants_base_test; import 'over_react/util/css_value_util_test.dart' as css_value_util_test; import 'over_react/util/dom_util_test.dart' as dom_util_test; import 'over_react/util/event_helpers_test.dart' as event_helpers_test; +import 'over_react/util/equality_test.dart' as equality_test; import 'over_react/util/guid_util_test.dart' as guid_util_test; import 'over_react/util/handler_chain_util_test.dart' as handler_chain_util_test; import 'over_react/util/hoc_test.dart' as hoc_test; @@ -52,6 +53,7 @@ void main() { css_value_util_test.main(); dom_util_test.main(); event_helpers_test.main(); + equality_test.main(); guid_util_test.main(); handler_chain_util_test.main(); hoc_test.main();