🚀 Emulate and test WebView
behaviors in node with
jest
and jsdom
(written in Typescript 💙)
The best use-case is when your application or library uses a
WebView
component with injected JavaScript. With Ersatz, you can mock theWebView
, perform assertions on the DOM, and verify that your JS to native communication is behaving as expected. Fundamentally, you can now-on consider code injected in aWebView
as part of the codebase and tested as deemed appropriate.
The easiest way to use Ersatz
is in combination with
jest
,
@formidable-webview/ersatz-testing
and
@testing-library/react-native
.
See examples
here. Because
there is no hard dependency on jest, You should be able to use Ersatz
with
any testing framework running on node, capable of mounting React components.
@formidable-webview/ersatz | react-native-webview |
---|---|
2.x | ≥ 7.6.0 < 12 |
All the props in common with ScrollView
are supported.
- Deprecated props are ignored.
- Platform-specific props are ignored.
Other props which can influence the DOM or the WebView and their support are listed bellow. If a prop is not listed, it is probably irrelevant (related to visual rendering) and will be ignored:
Prop | Support | Comments |
---|---|---|
source |
✔️ | Both remote URI (including body, headers and method) and inline HTML are supported. Local files are not supported. |
javaScriptEnabled |
✔️ | |
containerStyle |
✔️ | Mapped to ScrollView contentContainerStyle . |
renderError |
✔️ | |
renderLoading |
✔️ | |
injectedJavaScript |
✔️ | This code is evaluated in the DOM by jsdom. The code shouldn't be able to access scopes outside of the DOM, thanks to sandboxing. |
injectedJavaScriptBeforeContentLoaded |
✔️ | Ibidem. |
userAgent |
✔️ | |
injectedJavaScriptForMainFrameOnly |
Consider the behavior of Ersatz as if this prop was forced to true . |
|
injectedJavaScriptBeforeContentLoadedForMainFrameOnly |
Consider the behavior of Ersatz as if this prop was forced to true . |
|
incognito |
Technically, that is true because nothing is persisted between to jsdom instantiations! | |
allowsFullscreenVideo |
There are no visual rendering in jsdom, so this prop cannot be emulated. | |
cacheEnabled |
There is no cache implemented in jsdom. You can consider value false . |
|
javaScriptCanOpenWindowsAutomatically |
❌ | Navigation is not (yet) supported. |
mediaPlaybackRequiresUserAction |
❌ | |
originWhitelist |
❌ | |
startInLoadingState |
❌ | |
applicationNameForUserAgent |
❌ |
To reproduce the logic behind the different event handlers, we based our
assumptions on the doc, the original source code and manual testing in Android,
at version 10.3.3 of react-native-webview
. If you find a divergence in
behaviors, you are welcome to open a bug report. Bellow is a list of event
handler props with a description of our understanding of when this handler will
be invoked.
Event Handler | Support | Behavior |
---|---|---|
onLoad |
✔️ | Invoked when the WebView has finished the load operation with success. |
onLoadEnd |
✔️ | Invoked when the WebView has finished the load operation, either with a success or failure |
onError |
✔️ | Invoked when the WebView has finished the load operation with a failure. Failures cannot be reproduced though. |
onLoadStart |
✔️ | Invoked when the WebView is starting to load from a source object. |
onLoadProgress |
✔️ | Invoked multiple times when the WebView is loading. Although we support this, only one event will be fired at the end with progress: 1 . |
onHttpError |
✔️ | Invoked when a HTTP request fetching the resource in source.uri fails. We do provide the description and httpStatus attributes of nativeEvent . |
onMessage |
✔️ | Invoked when a script in the backend has posted a message with window.ReactNativeWebView.postMessage . We also provide the legacy window.postMessage . |
onNavigationStateChange |
Invoked when the WebView loading starts or ends. Special events such as formsubmit are not handled. Also, navigation to different URLs is not supported. |
|
onShouldStartLoadWithRequest |
❌ | Unsupported. We don't have navigation right now. |
onFileDownload |
❌ | Access to local filesystem is unsupported. |
Method | Support | Behavior |
---|---|---|
injectJavaScript |
✔️ | Full support. |
reload |
✔️ | Full support. |
stopLoading |
Method is present but does nothing. | |
goBack |
Method is present but does nothing. Navigation is not supported. | |
goForward |
Method is present but does nothing. Navigation is not supported. | |
requestFocus |
Method is present but does nothing. |
Method | Behavior |
---|---|
getDocument |
Return the Document instance loaded from the DOM. Prerequisite: the DOM must be loaded. |
getWindow |
Return the Window instance loaded from the DOM. Prerequisite: the DOM must be loaded. |
As per the manual tests we performed, a load cycle appears to start:
- When the component is mounted with a
source
prop; - When an attribute of
source
changes (a deep-equal don't trigger); - When the
reload
method is invoked; - When the
document.location.href
is changed (internal navigation); - When any event handler reference is changed;
FETCHING (0) LOAD START (1) PROGRESS (2) LOAD END (3)
onHttpError -> onLoadStart -> onLoadProgress -> onLoadEnd
onNavigationStateChange onNavigationStateChange
onLoad (success)
onError (failure)
According to the documentation, injectedJavaScriptBeforeContentLoaded
and
injectedScript
are loaded between (1) and (3), but
injectedJavaScriptBeforeContentLoaded
is run right after document object
creation, while injectedScript
is run after
DOMContentLoaded
event. Our manual tests found out that both scripts have access to
window.ReactNativeWebView
.
Edge cases
- When loaded with an undefined
source
prop, onLoadStart is triggered withloading: false
. - When loaded with an undefined
source
prop,injectedJavaScript
and other scripts are still run. - When loaded with a
source.url
prop,url
in events is set toabout:blank
.
- NativeSyntheticEvents types are respected, but outside of
nativeEvent
attribute, their content is meaningless. - The mounting of the DOM is performed with
jsdom. Thus, the assertions you can do on
the DOM are constrained by this library capabilities. Notably:
- because there is no real visual rendering performed, elements will have dimensions of size 0.
- the absence of a Navigation API. A detailed list of caveats is available here.