runJuiceTest<TBloc extends JuiceBloc<TState>, TState extends BlocState> function

Future<List<JuiceEmission<TState>>> runJuiceTest<TBloc extends JuiceBloc<TState>, TState extends BlocState>({
  1. required TBloc build(),
  2. TState seed()?,
  3. FutureOr<void> act(
    1. TBloc bloc
    )?,
  4. Duration? wait,
  5. int skip = 0,
  6. Object expect()?,
  7. Object errors()?,
  8. FutureOr<void> verify(
    1. TBloc bloc
    )?,
  9. bool allowInFlightAtClose = false,
})

The body of juiceTest, callable directly (e.g. inside testWidgets, or a test with custom setup). Returns the recorded emissions.

Implementation

Future<List<JuiceEmission<TState>>>
    runJuiceTest<TBloc extends JuiceBloc<TState>, TState extends BlocState>({
  required TBloc Function() build,
  TState Function()? seed,
  FutureOr<void> Function(TBloc bloc)? act,
  Duration? wait,
  int skip = 0,
  Object Function()? expect,
  Object Function()? errors,
  FutureOr<void> Function(TBloc bloc)? verify,
  bool allowInFlightAtClose = false,
}) async {
  final previousLogger = JuiceLoggerConfig.logger;
  final recorder = _RecordingLogger(previousLogger);
  JuiceLoggerConfig.configureLogger(recorder);

  final emissions = <JuiceEmission<TState>>[];
  void Function()? untap;
  var inFlightAtClose = 0;
  late final TBloc bloc;
  try {
    bloc = build();
    if (seed != null) {
      await bloc.send(UpdateEvent<TState>(newState: seed()));
    }
    // Recording starts after the seed.
    recorder.useCaseErrors.clear();
    untap = bloc.tapEmissions((status) {
      emissions.add(JuiceEmission<TState>(
        status,
        Set<String>.of(status.event?.groupsToRebuild ?? const <String>{}),
      ));
    });

    if (act != null) await act(bloc);
    if (wait != null) await Future<void>.delayed(wait);
  } finally {
    untap?.call();
    inFlightAtClose = recorder.openSpans.length;
    // Close BEFORE asserting: nothing emitted from here on can leak into
    // another test; in-flight work and late emissions are reported below.
    try {
      await bloc.close();
    } finally {
      JuiceLoggerConfig.configureLogger(previousLogger);
    }
  }

  final recorded = emissions.skip(skip).toList();
  if (expect != null) {
    final expected = expect();
    m.expect(recorded, _asEmissionsMatcher(expected),
        reason: 'juiceTest: emitted $recorded');
  }

  if (errors != null) {
    m.expect(recorder.useCaseErrors, m.wrapMatcher(errors()),
        reason: 'juiceTest: use-case errors');
  } else if (recorder.useCaseErrors.isNotEmpty) {
    m.fail('juiceTest: unexpected use-case error(s): '
        '${recorder.useCaseErrors}. Pass `errors:` to expect them.');
  }

  if (!allowInFlightAtClose &&
      (inFlightAtClose > 0 || recorder.emissionsAfterClose > 0)) {
    m.fail('juiceTest: $inFlightAtClose use case(s) still running when the '
        'bloc closed (${recorder.emissionsAfterClose} emission(s) dropped '
        'after close). Return the send() future from act, add `wait:`, or '
        'pass `allowInFlightAtClose: true`.');
  }

  if (verify != null) await verify(bloc);
  return emissions;
}