Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ public void setBasicAuthCredential(RNCBasicAuthCredential credential) {
mRNCWebViewClient.setBasicAuthCredential(credential);
}

/**
* E2E Testing: Enable E2E mode for request interception
*/
public void setE2EMode(boolean enabled) {
if (mRNCWebViewClient != null) {
mRNCWebViewClient.setE2EMode(enabled);
}
}

/**
* E2E Testing: Set mock server URL for request proxying
*/
public void setMockServerUrl(@Nullable String url) {
if (mRNCWebViewClient != null) {
mRNCWebViewClient.setMockServerUrl(url);
}
}

public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) {
this.sendContentSizeChangeEvents = sendContentSizeChangeEvents;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
Expand All @@ -47,6 +53,9 @@ public class RNCWebViewClient extends WebViewClient {
protected @Nullable String ignoreErrFailedForThisURL = null;
protected @Nullable RNCBasicAuthCredential basicAuthCredential = null;

protected boolean mE2EMode = false;
protected @Nullable String mMockServerUrl = null;

public void setIgnoreErrFailedForThisURL(@Nullable String url) {
ignoreErrFailedForThisURL = url;
}
Expand All @@ -55,6 +64,16 @@ public void setBasicAuthCredential(@Nullable RNCBasicAuthCredential credential)
basicAuthCredential = credential;
}

public void setE2EMode(boolean enabled) {
mE2EMode = enabled;
Log.d(TAG, "[E2E] E2E mode set to: " + enabled);
}

public void setMockServerUrl(@Nullable String url) {
mMockServerUrl = url;
Log.d(TAG, "[E2E] Mock server URL set to: " + url);
}

@Override
public void onPageFinished(WebView webView, String url) {
super.onPageFinished(webView, url);
Expand Down Expand Up @@ -167,6 +186,111 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) {
}
}

/**
* E2E Testing: Intercept all resource requests and route through mock server proxy.
* This allows the mock server to intercept and mock/block any network request from the WebView.
*/
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
// Only intercept in E2E mode with a valid mock server URL
if (!mE2EMode || mMockServerUrl == null) {
return super.shouldInterceptRequest(view, request);
}

String originalUrl = request.getUrl().toString();

// Skip interception for localhost/mock server URLs to avoid infinite loops
if (isLocalOrMockServerUrl(originalUrl)) {
return super.shouldInterceptRequest(view, request);
}

// Skip non-HTTP(S) URLs
if (!originalUrl.startsWith("http://") && !originalUrl.startsWith("https://")) {
return super.shouldInterceptRequest(view, request);
}

try {
// Route through mock server proxy (same pattern as shim.js)
String proxyUrl = mMockServerUrl + "/proxy?url=" + URLEncoder.encode(originalUrl, "UTF-8");
Log.d(TAG, "[E2E] Intercepting request: " + originalUrl + " -> " + proxyUrl);

URL url = new URL(proxyUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(request.getMethod());
connection.setConnectTimeout(30000);
connection.setReadTimeout(30000);

// Copy headers from original request
Map<String, String> requestHeaders = request.getRequestHeaders();
if (requestHeaders != null) {
for (Map.Entry<String, String> header : requestHeaders.entrySet()) {
// Skip host header as it will be set automatically
if (!"Host".equalsIgnoreCase(header.getKey())) {
connection.setRequestProperty(header.getKey(), header.getValue());
}
}
}

connection.connect();

int statusCode = connection.getResponseCode();
String contentType = connection.getContentType();
String encoding = connection.getContentEncoding();

// Get the appropriate input stream based on response code
InputStream inputStream;
if (statusCode >= 400) {
inputStream = connection.getErrorStream();
if (inputStream == null) {
inputStream = connection.getInputStream();
}
} else {
inputStream = connection.getInputStream();
}

// Parse content type and charset
String mimeType = "text/html";
String charset = "UTF-8";
if (contentType != null) {
String[] parts = contentType.split(";");
mimeType = parts[0].trim();
for (String part : parts) {
if (part.trim().toLowerCase().startsWith("charset=")) {
charset = part.trim().substring(8);
}
}
}

Log.d(TAG, "[E2E] Proxied response: status=" + statusCode + ", type=" + mimeType);
return new WebResourceResponse(mimeType, charset, statusCode,
connection.getResponseMessage(), connection.getHeaderFields().entrySet().stream()
.filter(e -> e.getKey() != null)
.collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))),
inputStream);

} catch (Exception e) {
Log.w(TAG, "[E2E] Failed to proxy request, falling back to original: " + e.getMessage());
// Fallback to original request on error
return super.shouldInterceptRequest(view, request);
}
}

/**
* Check if URL is local or pointing to mock server (should not be proxied)
*/
private boolean isLocalOrMockServerUrl(String url) {
try {
URL parsedUrl = new URL(url);
String host = parsedUrl.getHost();
return "localhost".equals(host) ||
"127.0.0.1".equals(host) ||
"10.0.2.2".equals(host) ||
(mMockServerUrl != null && url.startsWith(mMockServerUrl));
} catch (Exception e) {
return false;
}
}

@TargetApi(Build.VERSION_CODES.N)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,22 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) {
view.settings.allowFileAccessFromFileURLs = value;
}

/**
* E2E Testing: Enable E2E mode for request interception
*/
fun setE2EMode(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
val view = viewWrapper.webView
view.setE2EMode(enabled)
}

/**
* E2E Testing: Set mock server URL for request proxying
*/
fun setMockServerUrl(viewWrapper: RNCWebViewWrapper, url: String?) {
val view = viewWrapper.webView
view.setMockServerUrl(url)
}

fun setAllowsFullscreenVideo(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
mAllowsFullscreenVideo = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,16 @@ public void setPaymentRequestEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setPaymentRequestEnabled(view, value);
}

@ReactProp(name = "e2eMode")
public void setE2EMode(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setE2EMode(view, value);
}

@ReactProp(name = "mockServerUrl")
public void setMockServerUrl(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setMockServerUrl(view, value);
}

/* iOS PROPS - no implemented here */
@Override
public void setAllowingReadAccessToURL(RNCWebViewWrapper view, @Nullable String value) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,16 @@ public void setPaymentRequestEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setPaymentRequestEnabled(view, value);
}

@ReactProp(name = "e2eMode")
public void setE2EMode(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setE2EMode(view, value);
}

@ReactProp(name = "mockServerUrl")
public void setMockServerUrl(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setMockServerUrl(view, value);
}

@Override
protected void addEventEmitters(@NonNull ThemedReactContext reactContext, RNCWebViewWrapper viewWrapper) {
// Do not register default touch emitter and let WebView implementation handle touches
Expand Down
3 changes: 3 additions & 0 deletions apple/RNCWebViewImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
@property (nonatomic, assign) BOOL ignoreSilentHardwareSwitch;
@property (nonatomic, copy) NSString * _Nullable allowingReadAccessToURL;
@property (nonatomic, copy) NSDictionary * _Nullable basicAuthCredential;
// E2E Testing: Properties for request interception (currently handled via JS injection)
@property (nonatomic, assign) BOOL e2eMode;
@property (nonatomic, copy) NSString * _Nullable mockServerUrl;
@property (nonatomic, assign) BOOL pullToRefreshEnabled;
@property (nonatomic, assign) BOOL refreshControlLightMode;
@property (nonatomic, assign) BOOL enableApplePay;
Expand Down
3 changes: 3 additions & 0 deletions apple/RNCWebViewManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ - (RNCView *)view
RCT_EXPORT_VIEW_PROPERTY(allowsLinkPreview, BOOL)
RCT_EXPORT_VIEW_PROPERTY(allowingReadAccessToURL, NSString)
RCT_EXPORT_VIEW_PROPERTY(basicAuthCredential, NSDictionary)
// E2E Testing: Properties for request interception (currently handled via JS injection)
RCT_EXPORT_VIEW_PROPERTY(e2eMode, BOOL)
RCT_EXPORT_VIEW_PROPERTY(mockServerUrl, NSString)

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
RCT_EXPORT_VIEW_PROPERTY(contentInsetAdjustmentBehavior, UIScrollViewContentInsetAdjustmentBehavior)
Expand Down
Loading