Skip to content

Commit 1cc4ab3

Browse files
committed
chore: introduce E2E tests
1 parent 057a7ae commit 1cc4ab3

File tree

21 files changed

+344
-218
lines changed

21 files changed

+344
-218
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 121 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,11 @@ jobs:
3030
- name: Discover apps
3131
id: set-apps
3232
run: |
33-
# TODO: Implement app discovery logic
34-
# For now, we'll use a static list based on current structure
3533
if [ -n "${{ github.event.inputs.app }}" ]; then
3634
apps='["${{ github.event.inputs.app }}"]'
3735
else
38-
apps='["playground"]'
36+
# Dynamically discover apps in the /apps directory
37+
apps=$(find apps -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | jq -R . | jq -sc .)
3938
fi
4039
echo "apps=$apps" >> $GITHUB_OUTPUT
4140
echo "Discovered apps: $apps"
@@ -51,42 +50,81 @@ jobs:
5150
app: ${{ fromJson(needs.discover-apps.outputs.apps) }}
5251

5352
steps:
53+
- name: Delete unnecessary tools
54+
uses: jlumbroso/free-disk-space@v1.3.1
55+
with:
56+
android: false # Don't remove Android tools
57+
tool-cache: true # Remove image tool cache - rm -rf "$AGENT_TOOLSDIRECTORY"
58+
docker-images: false # Takes 16s, enable if needed in the future
59+
large-packages: false # includes google-cloud-sdk and it's slow
60+
5461
- name: Checkout code
5562
uses: actions/checkout@v4
5663

64+
- name: Install pnpm
65+
uses: pnpm/action-setup@v2
66+
with:
67+
version: latest
68+
5769
- name: Setup Node.js
5870
uses: actions/setup-node@v4
5971
with:
6072
node-version: '18'
6173
cache: 'pnpm'
6274

63-
- name: Install pnpm
64-
uses: pnpm/action-setup@v2
65-
with:
66-
version: latest
67-
6875
- name: Install dependencies
6976
run: |
70-
# TODO: Install project dependencies
71-
echo "Installing dependencies for ${{ matrix.app }}"
77+
pnpm install
78+
79+
- name: Set up JDK 17
80+
uses: actions/setup-java@v3
81+
with:
82+
java-version: '17'
83+
distribution: 'temurin'
7284

73-
- name: Setup Android environment
85+
- name: Restore APK from cache
86+
id: cache-apk-restore
87+
uses: actions/cache/restore@v4
88+
with:
89+
path: apps/${{ matrix.app }}/android/app/build/outputs/apk/debug/app-debug.apk
90+
key: apk-${{ matrix.app }}
91+
92+
- name: Build Android app
93+
if: steps.cache-apk-restore.outputs.cache-hit != 'true'
94+
working-directory: apps/${{ matrix.app }}
7495
run: |
75-
# TODO: Setup Android SDK, emulator, etc.
76-
echo "Setting up Android environment for ${{ matrix.app }}"
96+
pnpm nx run @react-native-harness/${{ matrix.app }}:build-android --tasks=assembleDebug
7797
78-
- name: Run E2E tests
98+
- name: Save APK to cache
99+
if: steps.cache-apk-restore.outputs.cache-hit != 'true' && success()
100+
uses: actions/cache/save@v4
101+
with:
102+
path: apps/${{ matrix.app }}/android/app/build/outputs/apk/debug/app-debug.apk
103+
key: apk-${{ matrix.app }}
104+
105+
- name: Enable KVM group perms
79106
run: |
80-
# TODO: Run E2E tests on Android
81-
echo "Running E2E tests for ${{ matrix.app }} on Android"
107+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
108+
sudo udevadm control --reload-rules
109+
sudo udevadm trigger --name-match=kvm
110+
ls /dev/kvm
82111
83-
- name: Upload test results
84-
if: always()
85-
uses: actions/upload-artifact@v4
112+
- name: Run E2E tests
113+
uses: reactivecircus/android-emulator-runner@v2
86114
with:
87-
name: e2e-results-android-${{ matrix.app }}
88-
path: test-results/
89-
retention-days: 7
115+
working-directory: apps/${{ matrix.app }}/android
116+
api-level: 35
117+
arch: x86_64
118+
profile: pixel_7
119+
disk-size: 1G
120+
heap-size: 1G
121+
force-avd-creation: false
122+
avd-name: Pixel_8_API_35
123+
disable-animations: true
124+
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
125+
script: |
126+
adb install -r "./app/build/outputs/apk/debug/app-debug.apk"
127+
pnpm nx run @react-native-harness/${{ matrix.app }}:start --args="test android-native"
90128
91129
e2e-ios:
92130
name: E2E iOS - ${{ matrix.app }}
@@ -102,52 +140,79 @@ jobs:
102140
- name: Checkout code
103141
uses: actions/checkout@v4
104142

143+
- name: Install pnpm
144+
uses: pnpm/action-setup@v2
145+
with:
146+
version: latest
147+
105148
- name: Setup Node.js
106149
uses: actions/setup-node@v4
107150
with:
108151
node-version: '18'
109152
cache: 'pnpm'
110153

111-
- name: Install pnpm
112-
uses: pnpm/action-setup@v2
154+
- name: Install dependencies
155+
run: |
156+
pnpm install
157+
158+
- name: Get build products directory
159+
id: get-build-dir
160+
working-directory: apps/${{ matrix.app }}/ios
161+
run: |
162+
BUILD_DIR=$(xcodebuild -showBuildSettings -workspace Playground.xcworkspace -scheme Playground -configuration Debug -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 16 Pro" 2>/dev/null | grep -w BUILT_PRODUCTS_DIR | awk '{print $3}')
163+
echo "build_dir=$BUILD_DIR" >> $GITHUB_OUTPUT
164+
echo "Build products directory: $BUILD_DIR"
165+
166+
- name: Restore app from cache
167+
id: cache-app-restore
168+
uses: actions/cache/restore@v4
113169
with:
114-
version: latest
170+
path: ${{ steps.get-build-dir.outputs.build_dir }}/Playground.app
171+
key: ios-app-${{ matrix.app }}
115172

116-
- name: Install dependencies
173+
- name: CocoaPods cache
174+
if: steps.cache-app-restore.outputs.cache-hit != 'true'
175+
uses: actions/cache@v4
176+
with:
177+
path: |
178+
./apps/${{ matrix.app }}/ios/Pods
179+
~/Library/Caches/CocoaPods
180+
~/.cocoapods
181+
key: ${{ matrix.app }}-${{ runner.os }}-pods-${{ hashFiles('./apps/${{ matrix.app }}/ios/Podfile.lock') }}
182+
restore-keys: |
183+
${{ matrix.app }}-${{ runner.os }}-pods-
184+
185+
- name: Install CocoaPods
186+
if: steps.cache-app-restore.outputs.cache-hit != 'true'
187+
working-directory: apps/${{ matrix.app }}/ios
117188
run: |
118-
# TODO: Install project dependencies
119-
echo "Installing dependencies for ${{ matrix.app }}"
189+
pod install
120190
121-
- name: Run E2E tests
191+
- name: Build iOS app
192+
if: steps.cache-app-restore.outputs.cache-hit != 'true'
193+
working-directory: apps/${{ matrix.app }}
122194
run: |
123-
# TODO: Run E2E tests on iOS
124-
echo "Running E2E tests for ${{ matrix.app }} on iOS"
195+
pnpm react-native build-ios
196+
197+
- name: Save app to cache
198+
if: steps.cache-app-restore.outputs.cache-hit != 'true' && success()
199+
uses: actions/cache/save@v4
200+
with:
201+
path: ${{ steps.get-build-dir.outputs.build_dir }}/Playground.app
202+
key: ios-app-${{ matrix.app }}
125203

126-
- name: Upload test results
127-
if: always()
128-
uses: actions/upload-artifact@v4
204+
- uses: futureware-tech/simulator-action@v4
129205
with:
130-
name: e2e-results-ios-${{ matrix.app }}
131-
path: test-results/
132-
retention-days: 7
206+
model: 'iPhone 16 Pro'
207+
os: iOS
208+
os_version: 18.2
209+
wait_for_boot: true
210+
erase_before_boot: false
133211

134-
e2e-summary:
135-
name: E2E Test Summary
136-
runs-on: ubuntu-latest
137-
needs: [e2e-android, e2e-ios]
138-
if: always()
139-
140-
steps:
141-
- name: Check test results
212+
- name: Install app
142213
run: |
143-
# TODO: Aggregate and report test results
144-
echo "E2E test summary:"
145-
echo "Android tests: ${{ needs.e2e-android.result }}"
146-
echo "iOS tests: ${{ needs.e2e-ios.result }}"
147-
148-
if [[ "${{ needs.e2e-android.result }}" == "failure" || "${{ needs.e2e-ios.result }}" == "failure" ]]; then
149-
echo "Some E2E tests failed"
150-
exit 1
151-
else
152-
echo "All E2E tests passed"
153-
fi
214+
xcrun simctl install booted ${{ steps.get-build-dir.outputs.build_dir }}/Playground.app
215+
216+
- name: Run E2E tests
217+
run: |
218+
pnpm nx run @react-native-harness/${{ matrix.app }}:start --args="test ios-native"

apps/playground/project.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
"start": {
44
"executor": "nx:run-commands",
55
"dependsOn": [
6-
"react-native-harness:build"
6+
"react-native-harness:build",
7+
"@react-native-harness/metro:build",
8+
"@react-native-harness/runtime:build"
79
],
810
"options": {
911
"command": "node ../../packages/cli/dist/index.js",

apps/playground/rn-harness.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const config = {
2-
include: '**/*.test.tsx',
2+
include: './withUI.test.tsx',
33

44
runners: [
55
{

apps/playground/withUI.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import { describe, it, expect, render, screen } from '@react-native-harness/runtime';
3+
import { View } from 'react-native';
4+
5+
describe('withUI', () => {
6+
it('should demonstrate UI interaction capability', async () => {
7+
try {
8+
await render(<View testID='testId' />);
9+
await screen.findByTestId('testId');
10+
} catch (error) {
11+
if (error.name === 'UIInteractionDisabledError') {
12+
// This is expected behavior for withUI: false runners
13+
expect(error.message).to.contain('Set "withUI: true"');
14+
} else {
15+
throw error; // Re-throw unexpected errors
16+
}
17+
}
18+
});
19+
20+
it('should work for native module testing without UI', () => {
21+
// This test should work regardless of withUI setting
22+
// as it doesn't use any interaction engine features
23+
const result = 2 + 2;
24+
expect(result).to.equal(4);
25+
});
26+
});

packages/bridge/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
}
2929
},
3030
"dependencies": {
31+
"@react-native-harness/tools": "workspace:*",
3132
"@react-native-harness/interaction-engine": "workspace:*",
3233
"birpc": "^2.4.0",
3334
"partysocket": "^1.1.4",
@@ -37,4 +38,4 @@
3738
"devDependencies": {
3839
"@types/ws": "^8.18.1"
3940
}
40-
}
41+
}

packages/bridge/src/server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { WebSocketServer, type WebSocket } from 'ws';
22
import { type BirpcGroup, createBirpcGroup } from 'birpc';
3+
import { logger } from '@react-native-harness/tools';
34
import { EventEmitter } from 'node:events';
45
import type { BridgeServerFunctions, BridgeClientFunctions } from './shared.js';
56

@@ -36,7 +37,7 @@ export const getBridgeServer = async ({
3637
port,
3738
}: BridgeServerOptions): Promise<BridgeServer> => {
3839
const wss = await new Promise<WebSocketServer>((resolve) => {
39-
const server = new WebSocketServer({ port }, () => {
40+
const server = new WebSocketServer({ port, host: '0.0.0.0' }, () => {
4041
resolve(server);
4142
});
4243
});
@@ -56,7 +57,10 @@ export const getBridgeServer = async ({
5657
);
5758

5859
wss.on('connection', (ws: WebSocket) => {
60+
logger.debug('Client connected to the bridge');
5961
ws.on('close', () => {
62+
logger.debug('Client disconnected from the bridge');
63+
6064
// TODO: Remove channel when connection is closed.
6165
clients.delete(ws);
6266
});

packages/bridge/tsconfig.lib.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
"references": [
1515
{
1616
"path": "../interaction-engine/tsconfig.lib.json"
17+
},
18+
{
19+
"path": "../tools/tsconfig.lib.json"
1720
}
1821
]
1922
}

packages/cli/src/bundlers/metro.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { type ChildProcess } from 'node:child_process';
2-
import { getTimeoutSignal, spawn } from '@react-native-harness/tools';
2+
import { getReactNativeCliPath, getTimeoutSignal, spawn } from '@react-native-harness/tools';
33

44
export const runMetro = async (): Promise<ChildProcess> => {
5-
const metro = spawn('react-native', ['start'], {
6-
stdio: 'ignore',
5+
const metro = spawn('node', [getReactNativeCliPath(), 'start'], {
76
env: {
87
...process.env,
98
RN_HARNESS: 'true',
109
},
1110
});
1211
const nodeChildProcess = await metro.nodeChildProcess;
1312

13+
nodeChildProcess.on('error', (error) => {
14+
console.error('Metro process error:', error);
15+
});
16+
1417
await waitForMetro();
1518
return nodeChildProcess;
1619
};

0 commit comments

Comments
 (0)