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
19 changes: 17 additions & 2 deletions app/components/Views/BrowserTab/BrowserTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ import { useMetrics } from '../../../components/hooks/useMetrics';
import { trackDappViewedEvent } from '../../../util/metrics';
import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics';
import { selectPermissionControllerState } from '../../../selectors/snaps/permissionController';
import { isTest } from '../../../util/test/utils.js';
import { isTest, isE2E } from '../../../util/test/utils.js';
import EntryScriptProxyE2E from '../../../core/EntryScriptProxyE2E';
import { EXTERNAL_LINK_TYPE } from '../../../constants/browser';
import { useNavigation } from '@react-navigation/native';
import { useStyles } from '../../hooks/useStyles';
Expand Down Expand Up @@ -519,8 +520,14 @@ export const BrowserTab: React.FC<BrowserTabProps> = React.memo(

const getEntryScriptWeb3 = async () => {
const entryScriptWeb3Fetched = await EntryScriptWeb3.get();

// In E2E mode, inject the proxy script FIRST to intercept all fetch/XHR calls
// This must run before any page scripts to ensure all requests are proxied
const e2eProxyScript = isE2E ? EntryScriptProxyE2E.get() : '';

setEntryScriptWeb3(
entryScriptWeb3Fetched +
e2eProxyScript +
entryScriptWeb3Fetched +
SPA_urlChangeListener +
SCROLL_TRACKER_SCRIPT,
);
Expand Down Expand Up @@ -1554,6 +1561,14 @@ export const BrowserTab: React.FC<BrowserTabProps> = React.memo(
onFileDownload={handleOnFileDownload}
webviewDebuggingEnabled={isTest}
paymentRequestEnabled
// E2E Testing: Enable native request interception on Android
// @ts-expect-error - Custom E2E props not in WebView types
e2eMode={isE2E}
mockServerUrl={
isE2E
? EntryScriptProxyE2E.getMockServerUrl()
: undefined
}
/>
{ipfsBannerVisible && (
<IpfsBanner
Expand Down
263 changes: 263 additions & 0 deletions app/core/EntryScriptProxyE2E.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import { Platform } from 'react-native';
import { LaunchArguments } from 'react-native-launch-arguments';
import {
isE2E,
FALLBACK_COMMAND_QUEUE_SERVER_PORT,
testConfig,
} from '../util/test/utils';
import { defaultMockPort } from '../../tests/api-mocking/mock-config/mockUrlCollection.json';

// Fallback port for mock server - same pattern as FixtureServer
// Android: Uses fallback port (adb reverse maps it to actual port)
// iOS: Uses actual port from LaunchArguments
const FALLBACK_MOCKSERVER_PORT = defaultMockPort || 8000;

/**
* The E2E proxy script that gets injected into the WebView.
* This script patches fetch and XMLHttpRequest to route through the mock server.
*
* This is embedded directly rather than loaded from a file to avoid
* complex build configuration changes.
*/
const INPAGE_PROXY_SCRIPT = `
(function () {
'use strict';

// Check if E2E config is available
if (!window.__E2E_MOCK_CONFIG__) {
return;
}

var config = window.__E2E_MOCK_CONFIG__;
var mockServerPort = config.mockServerPort;
var isAndroid = config.isAndroid;
var commandQueueServerPort = config.commandQueueServerPort;

var originalFetch = window.fetch;
var OriginalXHR = window.XMLHttpRequest;

// Try multiple hosts to find available mock server
var hosts = ['localhost'];
if (isAndroid) {
hosts.push('10.0.2.2');
}

var MOCKTTP_URL = '';
var isMockServerAvailable = false;

function isLocalUrl(url) {
try {
var parsed = new URL(url);
return (
parsed.hostname === 'localhost' ||
parsed.hostname === '127.0.0.1' ||
parsed.hostname === '10.0.2.2'
);
} catch (e) {
return false;
}
}

function isCommandQueueUrl(url) {
try {
var parsed = new URL(url);
return (
isLocalUrl(url) && parsed.port === String(commandQueueServerPort)
);
} catch (e) {
return false;
}
}

function isMockServerUrl(url) {
return (
url.includes('localhost:' + mockServerPort) ||
url.includes('10.0.2.2:' + mockServerPort) ||
url.includes('/proxy')
);
}

(async function init() {
for (var i = 0; i < hosts.length; i++) {
var host = hosts[i];
var testUrl = 'http://' + host + ':' + mockServerPort;

try {
var res = await originalFetch(testUrl + '/health-check');
if (res.ok) {
MOCKTTP_URL = testUrl;
isMockServerAvailable = true;
console.log('[E2E WebView] Mock server connected via ' + host);
break;
}
} catch (e) {
console.log('[E2E WebView] ' + host + ' health check failed: ' + e.message);
}
}

if (!isMockServerAvailable) {
console.warn('[E2E WebView] Mock server not available, using original fetch');
return;
}

// Patch fetch
window.fetch = async function (url, options) {
var urlString;
if (typeof url === 'string') {
urlString = url;
} else if (url instanceof URL) {
urlString = url.href;
} else if (url && typeof url === 'object' && url.url) {
urlString = url.url;
} else {
urlString = String(url);
}

if (isCommandQueueUrl(urlString) || isMockServerUrl(urlString)) {
return originalFetch(url, options);
}

if (urlString.startsWith('http://') || urlString.startsWith('https://')) {
try {
return await originalFetch(
MOCKTTP_URL + '/proxy?url=' + encodeURIComponent(urlString),
options
);
} catch (e) {
return originalFetch(url, options);
}
}

return originalFetch(url, options);
};

// Patch XMLHttpRequest
if (OriginalXHR) {
window.XMLHttpRequest = function () {
var xhr = new OriginalXHR();
var originalOpen = xhr.open;

xhr.open = function (method, url) {
var openArgs = Array.prototype.slice.call(arguments, 2);

try {
if (
typeof url === 'string' &&
(url.startsWith('http://') || url.startsWith('https://'))
) {
if (isCommandQueueUrl(url) || isMockServerUrl(url)) {
return originalOpen.apply(this, [method, url].concat(openArgs));
}
url = MOCKTTP_URL + '/proxy?url=' + encodeURIComponent(url);
}
return originalOpen.apply(this, [method, url].concat(openArgs));
} catch (error) {
return originalOpen.apply(this, [method, url].concat(openArgs));
}
};

return xhr;
};

try {
Object.setPrototypeOf(window.XMLHttpRequest, OriginalXHR);
Object.assign(window.XMLHttpRequest, OriginalXHR);
window.__E2E_XHR_PATCHED = true;
console.log('[E2E WebView] Successfully patched XMLHttpRequest');
} catch (error) {
console.warn('[E2E WebView] Failed to copy XHR properties:', error);
window.XMLHttpRequest = OriginalXHR;
}
}

console.log('[E2E WebView] Fetch and XHR patching complete');
})();
})();
`;

interface LaunchArgsType {
mockServerPort?: string;
commandQueueServerPort?: string;
}

interface TestConfigType {
fixtureServerPort?: number;
commandQueueServerPort?: number;
}

const EntryScriptProxyE2E = {
cachedScript: null as string | null,
cachedMockServerUrl: null as string | null,

/**
* Get the mock server port based on platform.
* Android: Uses fallback port (adb reverse maps to actual)
* iOS: Uses actual port from LaunchArguments
*/
getMockServerPort(): number {
const raw = LaunchArguments.value() as LaunchArgsType;
return Platform.OS === 'android'
? FALLBACK_MOCKSERVER_PORT
: Number(raw?.mockServerPort ?? FALLBACK_MOCKSERVER_PORT);
},

/**
* Get the mock server URL for native WebView interception.
* Returns the URL that the native code should use to proxy requests.
*/
getMockServerUrl(): string | undefined {
if (!isE2E) {
return undefined;
}

if (this.cachedMockServerUrl) {
return this.cachedMockServerUrl;
}

const port = this.getMockServerPort();
// Use localhost for both platforms - adb reverse handles Android mapping
this.cachedMockServerUrl = `http://localhost:${port}`;
return this.cachedMockServerUrl;
},

/**
* Get the E2E proxy script with configuration prepended.
* Returns empty string if not in E2E mode.
*/
get(): string {
// Only return script in E2E mode
if (!isE2E) {
return '';
}

// Return cached if available
if (this.cachedScript) {
return this.cachedScript;
}

const mockServerPort = this.getMockServerPort();
const commandQueueServerPort =
(testConfig as TestConfigType).commandQueueServerPort ??
FALLBACK_COMMAND_QUEUE_SERVER_PORT;

// Create the configuration that will be injected before the script
const config = `window.__E2E_MOCK_CONFIG__ = {
mockServerPort: ${mockServerPort},
isAndroid: ${Platform.OS === 'android'},
commandQueueServerPort: ${commandQueueServerPort}
};`;

this.cachedScript = config + INPAGE_PROXY_SCRIPT;
return this.cachedScript;
},

/**
* Clear the cached script and URL (useful for testing)
*/
clearCache(): void {
this.cachedScript = null;
this.cachedMockServerUrl = null;
},
};

export default EntryScriptProxyE2E;
2 changes: 1 addition & 1 deletion tests/api-mocking/MockServerE2E.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export default class MockServerE2E implements Resource {
return;
}

this._server = getLocal() as InternalMockServer;
this._server = getLocal({ cors: true }) as InternalMockServer;
this._server._liveRequests = [];
await this._server.start(this._serverPort);

Expand Down
Loading