Skip to content
134 changes: 65 additions & 69 deletions .github/workflows/e2e-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
workflow_dispatch:
jobs:
test:
runs-on: macos-12
runs-on: ubuntu-latest
timeout-minutes: 60
env:
WORKING_DIRECTORY: paper-example
Expand All @@ -25,87 +25,86 @@ jobs:
group: android-e2e-example-${{ github.ref }}
cancel-in-progress: true
steps:
- name: checkout
uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
- name: Checkout
uses: actions/checkout@v4

# - name: Free Disk Space (Ubuntu)
# uses: jlumbroso/free-disk-space@main
# with:
# tool-cache: true
# android: false

- name: Set up JDK 17
uses: actions/setup-java@v2
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'zulu'
cache: 'gradle'
- name: Install NDK
uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r26d
local-cache: true
- name: Set ANDROID_NDK
run: echo "ANDROID_NDK=$ANDROID_HOME/ndk-bundle" >> $GITHUB_ENV
- name: Cache SDK image
id: cache-sdk-img
uses: actions/cache@v3
with:
path: $ANDROID_HOME/system-images/
key: ${{ runner.os }}-build-system-images-${{ env.SYSTEM_IMAGES }}
- name: SKDs - download required images
if: ${{ steps.cache-sdd-img.outputs.cache-hit != 'true' }}
run: $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "system-images;android-34;google_apis;x86_64"
- name: Cache AVD
id: cache-avd
uses: actions/cache@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
path: ~/.android/avd/${{ env.AVD_NAME }}.avd
key: ${{ runner.os }}-avd-images-${{ env.SYSTEM_IMAGES }}-${{ env.AVD_NAME }}
- name: Emulator - Create
if: ${{ steps.cache-avd.outputs.cache-hit != 'true' }}
run: $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager create avd -n ${{ env.AVD_NAME }} --device 28 --package "${{ env.SYSTEM_IMAGES }}" --sdcard 512M
- name: Emulator - Set screen settings
if: ${{ steps.cache-avd.outputs.cache-hit != 'true' }}
node-version: 18
cache: 'yarn'

- name: Install AVD dependencies
# libxkbfile1 is removed by "Free Disk Space (Ubuntu)" step first. Here we install it again
# as it seems to be needed by the emulator.
run: |
echo "AVD config path: $HOME/.android/avd/${{ env.AVD_NAME }}.avd/config.ini"
sed -i '' 's/.*hw\.lcd\.density.*/hw\.lcd\.density = 480/g' $HOME/.android/avd/${{ env.AVD_NAME }}.avd/config.ini
sed -i '' 's/.*hw\.lcd\.width.*/hw\.lcd\.width = 1344/g' $HOME/.android/avd/${{ env.AVD_NAME }}.avd/config.ini
sed -i '' 's/.*hw\.lcd\.height.*/hw\.lcd\.height = 2992/g' $HOME/.android/avd/${{ env.AVD_NAME }}.avd/config.ini
- name: Emulator - Boot
run: $ANDROID_HOME/emulator/emulator -memory 4096 -avd ${{ env.AVD_NAME }} -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim &
sudo apt update
sudo apt-get install -y libpulse0 libgl1 libxkbfile1

- name: ADB Wait For Device
run: adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;'
timeout-minutes: 10
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Reverse TCP
working-directory: apps/${{ env.WORKING_DIRECTORY }}
run: adb devices | grep '\t' | awk '{print $1}' | sed 's/\\s//g' | xargs -I {} adb -s {} reverse tcp:8081 tcp:8081
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ env.API_LEVEL }}

- name: Install root node dependencies
run: yarn
- name: Run emulator, Metro, and E2E
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
target: default
profile: pixel_6
ram-size: '4096M'
disk-size: '5G'
disable-animations: false
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
avd-name: e2e_emulator
arch: x86_64
script: |
# Install root node dependencies
yarn install
# Install example app node dependencies
yarn --cwd apps/${{ env.WORKING_DIRECTORY }} install

- name: Install example app node dependencies
run: yarn
working-directory: apps/${{ env.WORKING_DIRECTORY }}
# Set up ADB reverse for Metro
$ANDROID_HOME/platform-tools/adb reverse tcp:8081 tcp:8081

- name: Build Android app
working-directory: apps/${{ env.WORKING_DIRECTORY }}/android
run: ./gradlew assembleDebug
# Start Metro in the background
E2E=true yarn --cwd apps/${{ env.WORKING_DIRECTORY }} start &> output.log &

- name: Start Metro server
working-directory: apps/${{ env.WORKING_DIRECTORY }}
run: E2E=true yarn start &> output.log &
# Build the Android app
cd apps/${{ env.WORKING_DIRECTORY }}/android && ./gradlew assembleDebug

- name: Install APK
run: adb install -r apps/${{ env.WORKING_DIRECTORY }}/android/app/build/outputs/apk/debug/app-debug.apk
# Install the app APK
$ANDROID_HOME/platform-tools/adb install -r apps/${{ env.WORKING_DIRECTORY }}/android/app/build/outputs/apk/debug/app-debug.apk

- name: Launch APK
run: 'while ! (adb shell monkey -p com.example 1 | grep -q "Events injected: 1"); do sleep 1; echo "Retrying due to errors in previous run..."; done'
# Launch the app using bash
bash -c 'until $ANDROID_HOME/platform-tools/adb shell monkey -p com.paperexample 1 | grep -q "Events injected: 1"; do sleep 1; echo "Retrying app launch..."; done'

- name: Run e2e Tests
run: E2E=true yarn e2e
# Run E2E tests
yarn e2e

- name: Upload test report
uses: actions/upload-artifact@v4
Expand All @@ -114,6 +113,3 @@ jobs:
path: |
report.html
jest-html-reporters-attach/

- name: Kill emulator (so it can be cached safely)
run: adb devices | grep emulator | cut -f1 | while read line; do adb -s $line emu kill; done
109 changes: 60 additions & 49 deletions apps/common/example/e2e/TestingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,59 +16,70 @@ export const TestingView = () => {
const [message, setMessage] = useState('⏳ Connecting to Jest server...');

const connect = useCallback(() => {
const client = new WebSocket(wsUri);
setWsClient(client);
setMessage('⏳ Connecting to Jest server...');
client.onopen = () => {
client.send(
JSON.stringify({
os: Platform.OS,
version: Platform.Version,
arch: isFabric() ? 'fabric' : 'paper',
connectionTime: new Date(),
}),
);
setMessage('✅ Connected to Jest server. Waiting for render requests.');
};
client.onerror = (err: any) => {
if (!err.message) {
return;
}
console.error(
`Error while connecting to E2E WebSocket server at ${wsUri}: ${err.message}. Will retry in 3 seconds.`,
);
setMessage(
`🚨 Failed to connect to Jest server at ${wsUri}: ${err.message}! Will retry in 3 seconds.`,
);
setTimeout(() => {
connect();
}, 3000);
};
client.onmessage = ({data: rawMessage}) => {
const message = JSON.parse(rawMessage);
if (message.type == 'renderRequest') {
setMessage(`✅ Rendering tests, please don't close this tab.`);
const {width, height} = message;
setResolution({width, height});
setRenderedContent(
createElementFromObject(
message.data.type || 'SvgFromXml',
message.data.props,
),
const startTime = Date.now();
const MAX_TIMEOUT = 10000;
let client = null;

const attemptConnect = () => {
client = new WebSocket(wsUri);
setWsClient(client);

client.onopen = () => {
client.send(
JSON.stringify({
os: Platform.OS,
version: Platform.Version,
arch: isFabric() ? 'fabric' : 'paper',
connectionTime: new Date(),
}),
);
setReadyToSnapshot(true);
}
};
client.onclose = event => {
if (event.code == 1006 && event.reason) {
// this is an error, let error handler take care of it
return;
}
setMessage(
`✅ Connection to Jest server has been closed. You can close this tab safely. (${event.code})`,
);

setMessage('✅ Connected to Jest server. Waiting for render requests.');
};

client.onerror = (err: any) => {
const elapsed = Date.now() - startTime;
if (elapsed >= MAX_TIMEOUT) {
setMessage(`❌ Failed to connect within ${MAX_TIMEOUT} milliseconds`);
return;
}

console.error(
`Error connecting to E2E WebSocket at ${wsUri}: ${
err.message ?? ''
}. Retrying...`,
);
setMessage(`🚨 Failed to connect: ${err.message ?? ''}. Retrying...`);
setTimeout(attemptConnect, 500);
};

client.onmessage = ({data: rawMessage}) => {
const message = JSON.parse(rawMessage);
if (message.type === 'renderRequest') {
setMessage(`✅ Rendering tests, please don't close this tab.`);
const {width, height} = message;
setResolution({width, height});
setRenderedContent(
createElementFromObject(
message.data.type || 'SvgFromXml',
message.data.props,
),
);
setReadyToSnapshot(true);
}
};

client.onclose = event => {
if (event.code === 1006 && event.reason) return;
setMessage(
`✅ Connection closed. You can close this tab safely. (${event.code})`,
);
};
};

attemptConnect();

return () => {
setWsClient(null);
client.close();
Expand Down
Loading