diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java index 3f9aa9c99..8546cdb70 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java @@ -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; } diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java index e77fc1512..a92c083bb 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java @@ -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; @@ -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; } @@ -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); @@ -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 requestHeaders = request.getRequestHeaders(); + if (requestHeaders != null) { + for (Map.Entry 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) { diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt index b19f45615..b98594244 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt @@ -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 diff --git a/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java index 6e21a020d..cb45ed977 100644 --- a/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -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) {} diff --git a/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java index f6469cd7c..5a38bd434 100644 --- a/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -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 diff --git a/apple/RNCWebViewImpl.h b/apple/RNCWebViewImpl.h index 49bbb3694..a997f4f1e 100644 --- a/apple/RNCWebViewImpl.h +++ b/apple/RNCWebViewImpl.h @@ -107,6 +107,9 @@ shouldStartLoadForRequest:(NSMutableDictionary *)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; diff --git a/apple/RNCWebViewManager.mm b/apple/RNCWebViewManager.mm index 96855c857..f74d13053 100644 --- a/apple/RNCWebViewManager.mm +++ b/apple/RNCWebViewManager.mm @@ -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)