Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2250a65
chore: add maestro tests to CI
kacperzolkiewski Mar 26, 2026
7096b2f
fix: maestro version
kacperzolkiewski Mar 26, 2026
466c2c5
fix: emulator start for CI
kacperzolkiewski Mar 26, 2026
f72b8a2
fix: run maestro server
kacperzolkiewski Mar 26, 2026
b120339
fix: andorid e2e
kacperzolkiewski Mar 26, 2026
a4de3d3
fix: restore cocoapods setup
kacperzolkiewski Mar 26, 2026
912a537
fix: cache derived data
kacperzolkiewski Mar 27, 2026
14cd6c4
fix: print available devices
kacperzolkiewski Mar 27, 2026
d6fd017
fix: set pixel_9 screen dimensions
kacperzolkiewski Mar 27, 2026
442c7c6
fix: increase timeout fo android emulator
kacperzolkiewski Mar 27, 2026
84d08e2
fix: add emulator logs to file
kacperzolkiewski Mar 27, 2026
c33f579
fix: reduce timeout
kacperzolkiewski Mar 27, 2026
f8096fe
fix: avd set
kacperzolkiewski Mar 27, 2026
5bcedbe
fix: pixel density
kacperzolkiewski Mar 27, 2026
d9ed9b9
fix: run android tests on pixel_7
kacperzolkiewski Mar 27, 2026
39ae9ed
fix: remove --skin for CI
kacperzolkiewski Mar 27, 2026
ddfeade
fix: make e2e tests run as nightly
kacperzolkiewski Mar 30, 2026
89348e9
fix: restore CI run on PR
kacperzolkiewski Mar 30, 2026
f729733
fix: remove passed e2e tests on ANdroid
kacperzolkiewski Mar 30, 2026
b5d30b1
fix: add link not extended
kacperzolkiewski Mar 30, 2026
6dccf89
fix: add exact path for diff pngs
kacperzolkiewski Mar 30, 2026
718befd
Merge branch 'main' into @kacperzolkiewski/add-e2e-tests-to-ci
kacperzolkiewski Mar 30, 2026
aae1a89
fix: restore test files
kacperzolkiewski Mar 31, 2026
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
191 changes: 191 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
name: E2E Tests
on:
push:
branches:
- main
pull_request:
branches:
- main
merge_group:
types:
- checks_requested

jobs:
changes:
runs-on: ubuntu-latest
outputs:
android: ${{ steps.filter.outputs.android }}
ios: ${{ steps.filter.outputs.ios }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Check file changes
uses: dorny/paths-filter@v3
id: filter
with:
filters: |
android:
- 'android/**'
- 'apps/example/android/**'
- 'apps/example/src/**'
- 'src/**'
- 'cpp/**'
- 'package.json'
- 'apps/example/package.json'
- 'react-native.config.js'
- 'babel.config.js'
- '.maestro/**'
ios:
- 'ios/**'
- 'apps/example/ios/**'
- 'apps/example/src/**'
- 'src/**'
- 'cpp/**'
- '*.podspec'
- 'package.json'
- 'apps/example/package.json'
- 'react-native.config.js'
- 'babel.config.js'
- '.maestro/**'

e2e-ios:
needs: [changes]
if: needs.changes.outputs.ios == 'true'
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup
uses: ./.github/actions/setup

- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Restore cocoapods
id: cocoapods-cache
uses: actions/cache/restore@v4
with:
path: |
**/ios/Pods
key: ${{ runner.os }}-cocoapods-${{ hashFiles('apps/example/ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-cocoapods-

- name: Install cocoapods
if: steps.cocoapods-cache.outputs.cache-hit != 'true'
run: |
cd apps/example/ios
pod install
env:
NO_FLIPPER: 1

- name: Cache cocoapods
if: steps.cocoapods-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
**/ios/Pods
key: ${{ steps.cocoapods-cache.outputs.cache-key }}

- name: Restore Xcode DerivedData
uses: actions/cache/restore@v4
id: derived-data-cache
with:
path: ~/Library/Developer/Xcode/DerivedData
key: ${{ runner.os }}-derived-data-${{ hashFiles('ios/**', 'apps/example/ios/**', 'cpp/**', 'src/**') }}
restore-keys: |
${{ runner.os }}-derived-data-

- name: Install Maestro CLI
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH

- name: Start Metro
run: yarn example start &

- name: Run E2E tests
run: yarn test:e2e:ios

- name: Cache Xcode DerivedData
if: steps.derived-data-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: ${{ steps.derived-data-cache.outputs.cache-primary-key }}

- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-ios-artifacts
path: |
.maestro/screenshots/ios/*_diff.png

e2e-android:
needs: [changes]
if: needs.changes.outputs.android == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup
uses: ./.github/actions/setup

- 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: Install JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'

- name: Finalize Android SDK
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "emulator" "platform-tools"
echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> $GITHUB_PATH
echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH
echo "$ANDROID_HOME/emulator" >> $GITHUB_PATH

- name: Install Maestro CLI
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH

- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/wrapper
~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('apps/example/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Start Metro
run: yarn example start &

- name: Run E2E tests
run: yarn test:e2e:android
env:
JAVA_OPTS: '-XX:MaxHeapSize=6g'

- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-android-artifacts
path: |
.maestro/screenshots/android/*_diff.png
Binary file modified .maestro/screenshots/android/empty_element_parsing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/screenshots/android/paragraph_styles_no_crash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/screenshots/android/scrolling_paragraph_styles_top.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions .maestro/scripts/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ if ! command -v maestro >/dev/null 2>&1; then
exit 1
fi

MAESTRO_VERSION=$(maestro --version)
MAESTRO_VERSION=$(maestro --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
# Compare versions by sorting them; if the minimum sorts after the actual, it's too old.
if [ "$(printf '%s\n' "$MIN_MAESTRO_VERSION" "$MAESTRO_VERSION" | sort -V | head -n1)" != "$MIN_MAESTRO_VERSION" ]; then
echo "Error: maestro $MAESTRO_VERSION is too old, minimum required is $MIN_MAESTRO_VERSION" >&2
Expand Down Expand Up @@ -60,7 +60,7 @@ case "$PLATFORM" in
*) echo "Error: --platform must be ios or android" >&2; exit 1 ;;
esac

DEVICE_ID=$("$SETUP" | tee /dev/tty | grep "^DEVICE_ID=" | cut -d= -f2)
DEVICE_ID=$("$SETUP" | tee /dev/stderr | grep "^DEVICE_ID=" | cut -d= -f2)

app_installed() {
if [ "$PLATFORM" = ios ]; then
Expand Down
31 changes: 21 additions & 10 deletions .maestro/scripts/setup-android-emulator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -euo pipefail

API_LEVEL="36"
DEVICE_ID="pixel_9"
DEVICE_ID="pixel_7"
ARCH=$(uname -m)
if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then
ABI="arm64-v8a"
Expand All @@ -11,7 +11,7 @@ else
fi
TAG="google_apis_playstore"
SYSTEM_IMAGE="system-images;android-${API_LEVEL};${TAG};${ABI}"
AVD_NAME="Pixel9-API${API_LEVEL}-Enriched"
AVD_NAME="Pixel7-API${API_LEVEL}-Enriched"
PORT=5570
SERIAL="emulator-${PORT}"

Expand All @@ -20,6 +20,11 @@ if [ -z "$ANDROID_HOME" ]; then
exit 1
fi

# Ensure avdmanager and emulator use the same AVD directory regardless of
# what ANDROID_SDK_HOME is set to on the host (e.g. GitHub Actions runners).
export ANDROID_AVD_HOME="$HOME/.android/avd"
mkdir -p "$ANDROID_AVD_HOME"

for tool in sdkmanager avdmanager emulator adb; do
if ! command -v "$tool" &>/dev/null; then
echo "Error: '$tool' not found. Ensure Android SDK tools are installed and in PATH."
Expand All @@ -41,19 +46,20 @@ fi

if ! avdmanager list avd -c | grep -qx "${AVD_NAME}"; then
echo "Creating AVD '$AVD_NAME'..."
echo "no" | avdmanager create avd \
--name "$AVD_NAME" \
--device "$DEVICE_ID" \
--package "$SYSTEM_IMAGE" \
--skin "$DEVICE_ID"
CREATE_CMD=(avdmanager create avd --name "$AVD_NAME" --device "$DEVICE_ID" --package "$SYSTEM_IMAGE")
# Skin is cosmetic (phone frame). Skip it on CI since the runner has no skin files
# and the emulator runs headless anyway.
[ -z "${CI:-}" ] && CREATE_CMD+=(--skin "$DEVICE_ID")
echo "no" | "${CREATE_CMD[@]}"
fi

AVD_CONFIG="$HOME/.android/avd/${AVD_NAME}.avd/config.ini"
if [ -f "$AVD_CONFIG" ]; then
sed -i '' 's/^hw\.keyboard=.*/hw.keyboard=yes/' "$AVD_CONFIG"
sed -i.bak 's/^hw\.keyboard=.*/hw.keyboard=yes/' "$AVD_CONFIG"
grep -q "^hw.keyboard=" "$AVD_CONFIG" || echo "hw.keyboard=yes" >> "$AVD_CONFIG"
sed -i '' 's/^hw\.mainKeys=.*/hw.mainKeys=yes/' "$AVD_CONFIG"
sed -i.bak 's/^hw\.mainKeys=.*/hw.mainKeys=yes/' "$AVD_CONFIG"
grep -q "^hw.mainKeys=" "$AVD_CONFIG" || echo "hw.mainKeys=yes" >> "$AVD_CONFIG"
rm -f "$AVD_CONFIG.bak"
fi

if pgrep -f "emulator.*${AVD_NAME}" > /dev/null 2>&1; then
Expand All @@ -63,7 +69,12 @@ if pgrep -f "emulator.*${AVD_NAME}" > /dev/null 2>&1; then
fi

echo "Starting emulator '$AVD_NAME'..."
emulator "@${AVD_NAME}" -port "$PORT" > /dev/null 2>&1 &
EMULATOR_ARGS=("@${AVD_NAME}" -port "$PORT")
if [ -n "${CI:-}" ]; then
EMULATOR_ARGS+=(-no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim)
fi

emulator "${EMULATOR_ARGS[@]}" > /dev/null 2>&1 &

echo "Waiting for emulator ($SERIAL) to connect to ADB..."
if ! timeout 120 adb -s "$SERIAL" wait-for-device; then
Expand Down
4 changes: 3 additions & 1 deletion .maestro/scripts/setup-ios-simulator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ if [ "$STATE" != "(Booted)" ]; then
xcrun simctl boot "$UDID"
fi

open -a Simulator
if [ -z "${CI:-}" ]; then
open -a Simulator
fi

echo "Simulator ready: $DEVICE_NAME ($UDID)"
echo "DEVICE_ID=$UDID"
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ The target devices are:
| Platform | Device | OS |
| -------- | --------- | ----------------------------- |
| iOS | iPhone 17 | iOS 26.2 |
| Android | Pixel 9 | API 36 "Baklava" (Android 16) |
| Android | Pixel 7 | API 36 "Baklava" (Android 16) |

#### Running E2E tests

Expand Down
Loading