Skip to content

Commit

Permalink
ConnectorTester on the Store.
Browse files Browse the repository at this point in the history
  • Loading branch information
marcglasberg committed May 8, 2024
1 parent fa1913e commit 341afdf
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 68 deletions.
62 changes: 62 additions & 0 deletions lib/src/connector_tester.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg
// For more info, see: https://pub.dartlang.org/packages/async_redux

import 'package:flutter/material.dart' hide Action;

import '../async_redux.dart';

/// Helps testing the `StoreConnector`s methods, such as `onInit`,
/// `onDispose` and `onWillChange`.
///
/// For more info, see: https://pub.dartlang.org/packages/async_redux
///
/// Example: Suppose you have a `StoreConnector` which dispatches `SomeAction`
/// on its `onInit`. How could you test that?
///
/// ```
/// class MyConnector extends StatelessWidget {
/// Widget build(BuildContext context) => StoreConnector<AppState, Vm>(
/// vm: () => _Factory(),
/// onInit: _onInit,
/// builder: (context, vm) { ... }
/// }
///
/// void _onInit(Store<AppState> store) => store.dispatch(SomeAction());
/// }
///
/// var storeTester = StoreTester(...);
/// ConnectorTester(tester, MyConnector()).runOnInit();
/// var info = await tester.waitUntil(SomeAction);
/// ```
///
class ConnectorTester<St, Model> {
final Store<St> store;
final StatelessWidget widgetConnector;

StoreConnector<St, Model>? _storeConnector;

StoreConnector<St, Model> get storeConnector => _storeConnector ??=
// ignore: invalid_use_of_protected_member
widgetConnector.build(StatelessElement(widgetConnector)) as StoreConnector<St, Model>;

ConnectorTester(this.store, this.widgetConnector);

void runOnInit() {
final OnInitCallback<St>? onInit = storeConnector.onInit;
if (onInit != null) onInit(store);
}

void runOnDispose() {
final OnDisposeCallback<St>? onDispose = storeConnector.onDispose;
if (onDispose != null) onDispose(store);
}

void runOnWillChange(
Model previousVm,
Model newVm,
) {
final OnWillChangeCallback<St, Model>? onWillChange = storeConnector.onWillChange;
if (onWillChange != null)
onWillChange(StatelessElement(widgetConnector), store, previousVm, newVm);
}
}
63 changes: 52 additions & 11 deletions lib/src/store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import 'package:async_redux/async_redux.dart';
import 'package:async_redux/src/process_persistence.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import 'connector_tester.dart';

part 'redux_action.dart';

Expand Down Expand Up @@ -437,10 +440,14 @@ class Store<St> {

/// Returns a future which will complete when the given state [condition] is true.
///
/// If [completeImmediately] is `true` (the default), the future will complete immediately and
/// throw no error. Otherwise, this method will throw [StoreException] if the condition is
/// already true when the method is called. Note: The default here is `true`, while in the
/// other `wait` methods like [waitActionCondition] it's `false`. This makes sense because of
/// If [completeImmediately] is `true` (the default) and the condition was already true when
/// the method was called, the future will complete immediately and throw no errors.
///
/// If [completeImmediately] is `false` and the condition was already true when
/// the method was called, it will throw a [StoreException].
///
/// Note: The default here is `true`, while in the other `wait` methods
/// like [waitActionCondition] it's `false`. This makes sense because of
/// the different use cases for these methods.
///
/// You may also provide a [timeoutMillis], which by default is 10 minutes.
Expand Down Expand Up @@ -526,11 +533,15 @@ class Store<St> {
Future<ReduxAction<St>?> waitCondition(
bool Function(St) condition, {
//
/// If `completeImmediately` is `true` (the default), the future will complete
/// immediately and throw no error. Otherwise, this method will throw [StoreException]
/// if the condition is already true when the method is called. Note: The default here
/// is `true`, while in the other `wait` methods like [waitActionCondition] it's `false`.
/// This makes sense because of the different use cases for these methods.
/// If `completeImmediately` is `true` (the default) and the condition was already true when
/// the method was called, the future will complete immediately and throw no errors.
///
/// If `completeImmediately` is `false` and the condition was already true when
/// the method was called, it will throw a [StoreException].
///
/// Note: The default here is `true`, while in the other `wait` methods
/// like [waitActionCondition] it's `false`. This makes sense because of
/// the different use cases for these methods.
bool completeImmediately = true,
//
/// The maximum time to wait for the condition to be met. The default is 10 minutes.
Expand Down Expand Up @@ -941,8 +952,8 @@ class Store<St> {
return triggerAction;
}

/// Returns a future that completes when ALL actions of the given type are NOT in progress
/// (none of them is being dispatched):
/// Returns a future that completes when ALL actions of the given types are NOT in progress
/// (none of them are being dispatched):
///
/// - If NO action of the given types is currently in progress when the method is called,
/// and [completeImmediately] is `false` (the default), this method will throw an error.
Expand Down Expand Up @@ -1386,6 +1397,9 @@ class Store<St> {
}
}

/// Returns a copy of the error queue, containing user exception errors thrown by
/// dispatched actions. Note that this is a copy of the queue, so you can't modify the original
/// queue here. Instead, use [getAndRemoveFirstError] to consume the errors, one by one.
Queue<UserException> get errors => Queue<UserException>.of(_errors);

/// We check the return type of methods `before` and `reduce` to decide if the
Expand Down Expand Up @@ -2071,6 +2085,33 @@ class Store<St> {
return _changeController.close();
}

/// Helps testing the `StoreConnector`s methods, such as `onInit`,
/// `onDispose` and `onWillChange`.
///
/// For example, suppose you have a `StoreConnector` which dispatches
/// `SomeAction` on its `onInit`. How could you test that?
///
/// ```
/// class MyConnector extends StatelessWidget {
/// Widget build(BuildContext context) => StoreConnector<AppState, Vm>(
/// vm: () => _Factory(),
/// onInit: _onInit,
/// builder: (context, vm) { ... }
/// }
///
/// void _onInit(Store<AppState> store) => store.dispatch(SomeAction());
/// }
///
/// var store = Store(...);
/// var connectorTester = store.getConnectorTester(MyConnector());
/// connectorTester.runOnInit();
/// var action = await store.waitAnyActionTypeFinishes([SomeAction]);
/// expect(action.someValue, 123);
/// ```
///
ConnectorTester<St, Model> getConnectorTester<Model>(StatelessWidget widgetConnector) =>
ConnectorTester<St, Model>(this, widgetConnector);

/// Throws the error after an asynchronous gap.
void _throws(errorMsg, Object? error, StackTrace stackTrace) {
Future(() {
Expand Down
59 changes: 2 additions & 57 deletions lib/src/store_tester.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart' hide Action;

import '../async_redux.dart';
import 'connector_tester.dart';

/// Predicate used in [StoreTester.waitCondition].
/// Return true to stop waiting, and get the last state.
Expand Down Expand Up @@ -792,63 +793,7 @@ class StoreTester<St> {
/// ```
///
ConnectorTester<St, Model> getConnectorTester<Model>(StatelessWidget widgetConnector) =>
ConnectorTester<St, Model>(this, widgetConnector);
}

/// Helps testing the `StoreConnector`s methods, such as `onInit`,
/// `onDispose` and `onWillChange`.
///
/// For more info, see: https://pub.dartlang.org/packages/async_redux
///
/// Example: Suppose you have a `StoreConnector` which dispatches `SomeAction`
/// on its `onInit`. How could you test that?
///
/// ```
/// class MyConnector extends StatelessWidget {
/// Widget build(BuildContext context) => StoreConnector<AppState, Vm>(
/// vm: () => _Factory(),
/// onInit: _onInit,
/// builder: (context, vm) { ... }
/// }
///
/// void _onInit(Store<AppState> store) => store.dispatch(SomeAction());
/// }
///
/// var storeTester = StoreTester(...);
/// ConnectorTester(tester, MyConnector()).runOnInit();
/// var info = await tester.waitUntil(SomeAction);
/// ```
///
class ConnectorTester<St, Model> {
final StoreTester<St> tester;
final StatelessWidget widgetConnector;

StoreConnector<St, Model>? _storeConnector;

StoreConnector<St, Model> get storeConnector => _storeConnector ??=
// ignore: invalid_use_of_protected_member
widgetConnector.build(StatelessElement(widgetConnector)) as StoreConnector<St, Model>;

ConnectorTester(this.tester, this.widgetConnector);

void runOnInit() {
final OnInitCallback<St>? onInit = storeConnector.onInit;
if (onInit != null) onInit(tester.store);
}

void runOnDispose() {
final OnDisposeCallback<St>? onDispose = storeConnector.onDispose;
if (onDispose != null) onDispose(tester.store);
}

void runOnWillChange(
Model previousVm,
Model newVm,
) {
final OnWillChangeCallback<St, Model>? onWillChange = storeConnector.onWillChange;
if (onWillChange != null)
onWillChange(StatelessElement(widgetConnector), tester.store, previousVm, newVm);
}
ConnectorTester<St, Model>(store, widgetConnector);
}

/// List of test information, before or after some actions are dispatched.
Expand Down

0 comments on commit 341afdf

Please sign in to comment.