waitFor<T> function Null safety Async

Future<T> waitFor<T>(
  1. FutureOr<T> expectation(
      ),
    1. {Node? container,
    2. Duration? timeout,
    3. Duration interval = defaultAsyncCallbackCheckInterval,
    4. QueryTimeoutFn? onTimeout,
    5. MutationObserverOptions mutationObserverOptions = defaultMutationObserverOptions}
    )

    Calls the provided expectation on a given interval and/or when the container DOM changes, completing only if it does not throw, or by throwing if the timeout expires before the expectation succeeds.

    Similar to testing-library.com/docs/dom-testing-library/api-async/#waitfor, but designed to work with the dart:test package's expect function and Dart Futures instead of JS Promises.

    • If you're waiting for an element to exist in the DOM, use a findBy* query instead.
    • If you're waiting for an element to be removed from the DOM, use waitForElementToBeRemoved instead.

    Options

    container

    The DOM node to attach the MutationObserver to.

    Defaults to document.body.

    timeout

    How long to wait for the node to appear in the DOM before throwing a TestFailure, defaulting to 1000ms.

    interval

    How often the callback is called, defaulting to 50ms.

    onTimeout

    Is called if the timeout duration passes before the node is found in the DOM, and can be used to customize a TestFailure message.

    mutationObserverOptions

    The default values are:

    {subtree: true, childList: true, attributes: true, characterData: true}
    

    which will detect additions and removals of child elements (including text nodes) in the container and any of its descendants. It will also detect attribute changes. When any of those changes occur, it will re-run the callback.

    Implementation

    Future<T> waitFor<T>(
      FutureOr<T> Function() expectation, {
      Node? container,
      Duration? timeout,
      Duration interval = defaultAsyncCallbackCheckInterval,
      QueryTimeoutFn? onTimeout,
      MutationObserverOptions mutationObserverOptions = defaultMutationObserverOptions,
    }) async {
      final config = getConfig();
      container ??= document.body!;
      timeout ??= Duration(milliseconds: config.asyncUtilTimeout);
      onTimeout ??= (error) => error;
    
      /*Error*/ Object? lastError;
      late MutationObserver observer;
      late Timer intervalTimer;
      late Timer overallTimeoutTimer;
      var isPending = false;
      final doneCompleter = Completer<T>();
    
      void onDone(T result) {
        if (doneCompleter.isCompleted) return;
    
        overallTimeoutTimer.cancel();
        intervalTimer.cancel();
        observer.disconnect();
    
        if (result is TestFailure) {
          doneCompleter.completeError(result);
        } else {
          doneCompleter.complete(result);
        }
      }
    
      // Separate error handling to enforce non-nullability of the result.
      void onDoneWithError(Object error) {
        if (doneCompleter.isCompleted) return;
    
        overallTimeoutTimer.cancel();
        intervalTimer.cancel();
        observer.disconnect();
    
        doneCompleter.completeError(error);
      }
    
      void handleTimeout() {
        final error = lastError ?? TimeoutException('Timed out in waitFor after ${timeout!.inMilliseconds}ms.');
        onDoneWithError(onTimeout!(error));
      }
    
      void checkCallback() {
        if (isPending) return;
        try {
          final result = expectation();
          if (result is Future) {
            isPending = true;
            (result! as Future)
                .then((resolvedValue) => onDone(resolvedValue as T), onError: (e) => lastError = e)
                .whenComplete(() => isPending = false);
          } else {
            onDone(result);
          }
          // If `callback` throws, wait for the next mutation, interval, or timeout.
        } catch (error) {
          // Save the most recent callback error to reject the promise with it in the event of a timeout
          lastError = error;
        }
      }
    
      overallTimeoutTimer = Timer(timeout, handleTimeout);
      intervalTimer = Timer.periodic(interval, (_) => checkCallback());
      observer = MutationObserver((_, __) => checkCallback())
        ..observe(
          container,
          childList: mutationObserverOptions.childList,
          attributes: mutationObserverOptions.attributes,
          characterData: mutationObserverOptions.characterData,
          subtree: mutationObserverOptions.subtree,
          attributeFilter: mutationObserverOptions.attributeFilter,
        );
      checkCallback();
    
      return doneCompleter.future;
    }