waitFor<T> function
Null safety
Async
Calls the provided expectation
on a given interval
and/or when the container
DOM changes,
completing only if it does not throw
, or by throw
ing 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 Promise
s.
- 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;
}