Skip to content

Commit 8418a37

Browse files
authored
feat: support for web (#62)
Added support for web platform with all functionalities supported by the native equivalents, including UI testing capabilities.
1 parent 0f0ca1b commit 8418a37

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1722
-2055
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ on:
1212
- all
1313
- android
1414
- ios
15+
- web
1516
pull_request:
1617
types: [ready_for_review]
1718
branches:
@@ -189,3 +190,50 @@ jobs:
189190
app: ios/build/Build/Products/Debug-iphonesimulator/HarnessPlayground.app
190191
runner: ios
191192
projectRoot: apps/playground
193+
194+
e2e-web:
195+
name: E2E Web
196+
runs-on: ubuntu-22.04
197+
if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main') || (github.event_name == 'workflow_dispatch' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web')) }}
198+
env:
199+
HARNESS_DEBUG: true
200+
201+
steps:
202+
- name: Checkout code
203+
uses: actions/checkout@v4
204+
with:
205+
ref: ${{ github.event.pull_request.head.sha || github.ref }}
206+
fetch-depth: 0
207+
208+
- name: Install pnpm
209+
uses: pnpm/action-setup@v2
210+
with:
211+
version: latest
212+
213+
- name: Setup Node.js
214+
uses: actions/setup-node@v4
215+
with:
216+
node-version: '24.10.0'
217+
cache: 'pnpm'
218+
219+
- name: Metro cache
220+
uses: actions/cache@v4
221+
with:
222+
path: apps/playground/node_modules/.cache/rn-harness/metro-cache
223+
key: metro-cache-${{ hashFiles('apps/playground/node_modules/.cache/rn-harness/metro-cache/**/*') }}
224+
restore-keys: |
225+
metro-cache
226+
227+
- name: Install dependencies
228+
run: |
229+
pnpm install
230+
231+
- name: Build packages
232+
run: |
233+
pnpm nx run-many -t build --projects="packages/*"
234+
235+
- name: Run React Native Harness
236+
uses: ./actions/web
237+
with:
238+
runner: chromium
239+
projectRoot: apps/playground

.npmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
strict-peer-dependencies=false
2-
auto-install-peers=true
2+
auto-install-peers=false
33
node-linker=hoisted
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
__default__: prerelease
3+
---
4+
5+
Added support for web platform with all functionalities supported by the native equivalents, including UI testing capabilities.

actions/web/action.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: React Native Harness for Web
2+
description: Run React Native Harness tests on Web
3+
inputs:
4+
runner:
5+
description: The runner to use
6+
required: true
7+
type: string
8+
projectRoot:
9+
description: The project root directory
10+
required: false
11+
type: string
12+
uploadVisualTestArtifacts:
13+
description: Whether to upload visual test diff and actual images as artifacts
14+
required: false
15+
type: boolean
16+
default: 'true'
17+
runs:
18+
using: 'composite'
19+
steps:
20+
- name: Load React Native Harness configuration
21+
id: load-config
22+
shell: bash
23+
env:
24+
INPUT_RUNNER: ${{ inputs.runner }}
25+
INPUT_PROJECTROOT: ${{ inputs.projectRoot }}
26+
run: |
27+
node ${{ github.action_path }}/../shared/index.cjs
28+
- name: Install Playwright Browsers
29+
shell: bash
30+
run: npx playwright install --with-deps chromium
31+
- name: Run E2E tests
32+
shell: bash
33+
working-directory: ${{ inputs.projectRoot }}
34+
run: |
35+
pnpm react-native-harness --harnessRunner ${{ inputs.runner }}
36+
- name: Upload visual test artifacts
37+
if: always() && inputs.uploadVisualTestArtifacts == 'true'
38+
uses: actions/upload-artifact@v4
39+
with:
40+
name: visual-test-diffs-chromium
41+
path: |
42+
${{ inputs.projectRoot }}/**/__image_snapshots__/**/*-diff.png
43+
${{ inputs.projectRoot }}/**/__image_snapshots__/**/*-actual.png
44+
if-no-files-found: ignore

actions/web/index.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"use strict";

apps/playground/index.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Harness Playground</title>
6+
<meta name="viewport" content="width=device-width,initial-scale=1" />
7+
<style>
8+
html,
9+
body {
10+
height: 100%;
11+
}
12+
13+
body {
14+
overflow: hidden;
15+
}
16+
17+
#root {
18+
display: flex;
19+
height: 100%;
20+
}
21+
</style>
22+
</head>
23+
<body>
24+
<div id="root"></div>
25+
<script src="http://localhost:8081/index.bundle?platform=web&dev=true&entryFile=index.js"></script>
26+
</body>
27+
</html>

apps/playground/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
* @format
33
*/
44

5-
import { AppRegistry } from 'react-native';
5+
import { AppRegistry, Platform } from 'react-native';
66
import App from './src/app/App';
77
import { name as appName } from './app.json';
88

99
AppRegistry.registerComponent(appName, () => App);
10+
11+
if (Platform.OS === 'web') {
12+
AppRegistry.runApplication(appName, {
13+
rootTag: document.getElementById('root'),
14+
});
15+
}

apps/playground/metro.config.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
11
const { withNxMetro } = require('@nx/react-native');
22
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
33
const path = require('path');
4+
const fs = require('fs');
45

56
const defaultConfig = getDefaultConfig(__dirname);
67

78
const projectRoot = __dirname;
89
const monorepoRoot = path.resolve(projectRoot, '../..');
910

11+
const getCustomResolver = (defaultResolveRequest) => (context, moduleName, platform) => {
12+
if (platform === 'web') {
13+
if (moduleName.includes('NativeSourceCode') ||
14+
moduleName.includes('NativePlatformConstants') ||
15+
moduleName.includes('NativeDevSettings') ||
16+
moduleName.includes('NativeLogBox') ||
17+
moduleName.includes('NativeRedBox')
18+
) {
19+
return {
20+
type: 'empty',
21+
};
22+
} else if (moduleName === 'react-native') {
23+
return {
24+
type: 'sourceFile',
25+
filePath: require.resolve('react-native-web'),
26+
};
27+
}
28+
}
29+
30+
// Everything else: default behavior
31+
return defaultResolveRequest(context, moduleName, platform);
32+
};
33+
1034
/**
1135
* Metro configuration
1236
* https://reactnative.dev/docs/metro
@@ -18,6 +42,30 @@ const customConfig = {
1842
resolver: {
1943
unstable_enablePackageExports: true,
2044
},
45+
server: {
46+
...(defaultConfig.server || {}),
47+
enhanceMiddleware: (middleware) => {
48+
return (req, res, next) => {
49+
if (req.url === '/' || req.url === '/index.html') {
50+
const htmlPath = path.join(projectRoot, 'index.html');
51+
52+
fs.readFile(htmlPath, 'utf8', (err, data) => {
53+
if (err) {
54+
res.writeHead(500, { 'Content-Type': 'text/plain' });
55+
res.end('Error loading index.html: ' + err.message);
56+
return;
57+
}
58+
59+
res.writeHead(200, { 'Content-Type': 'text/html' });
60+
res.end(data);
61+
});
62+
return;
63+
}
64+
65+
return middleware(req, res, next);
66+
};
67+
},
68+
},
2169
};
2270

2371
module.exports = withNxMetro(mergeConfig(defaultConfig, customConfig), {
@@ -26,4 +74,8 @@ module.exports = withNxMetro(mergeConfig(defaultConfig, customConfig), {
2674
path.resolve(projectRoot, 'node_modules'),
2775
path.resolve(monorepoRoot, 'node_modules'),
2876
],
77+
}).then((config) => {
78+
// Nx overrides the resolveRequest, so we need to override it after the merge.
79+
config.resolver.resolveRequest = getCustomResolver(config.resolver.resolveRequest);
80+
return config;
2981
});

apps/playground/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
"test:harness": "jest --selectProjects react-native-harness"
77
},
88
"dependencies": {
9-
"react": "19.1.1",
10-
"react-native": "0.82.1"
9+
"react": "19.2.3",
10+
"react-dom": "19.2.3",
11+
"react-native": "0.82.1",
12+
"react-native-web": "^0.21.2"
1113
},
1214
"devDependencies": {
1315
"react-native-harness": "workspace:*",
@@ -24,6 +26,7 @@
2426
"jest": "^30.2.0",
2527
"@react-native-harness/platform-android": "workspace:*",
2628
"@react-native-harness/platform-apple": "workspace:*",
27-
"@react-native-harness/platform-vega": "workspace:*"
29+
"@react-native-harness/platform-vega": "workspace:*",
30+
"@react-native-harness/platform-web": "workspace:*"
2831
}
2932
}

apps/playground/rn-harness.config.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import {
1212
vegaPlatform,
1313
vegaEmulator,
1414
} from '@react-native-harness/platform-vega';
15+
import {
16+
webPlatform,
17+
chromium,
18+
chrome,
19+
} from '@react-native-harness/platform-web';
1520

1621
const config = {
1722
entryPoint: './index.js',
@@ -48,6 +53,14 @@ const config = {
4853
device: vegaEmulator('VegaTV_1'),
4954
bundleId: 'com.playground',
5055
}),
56+
webPlatform({
57+
name: 'web',
58+
browser: chrome('http://localhost:8081/index.html', { headless: false }),
59+
}),
60+
webPlatform({
61+
name: 'chromium',
62+
browser: chromium('http://localhost:8081/index.html', { headless: true }),
63+
}),
5164
],
5265
defaultRunner: 'android',
5366
bridgeTimeout: 120000,

0 commit comments

Comments
 (0)