From 2250a6501df2062bba30c2eb83b182aca63dc684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 26 Mar 2026 17:08:06 +0100 Subject: [PATCH 01/22] chore: add maestro tests to CI --- .github/workflows/e2e.yml | 168 +++++++++++++++++++++ .maestro/scripts/setup-android-emulator.sh | 11 +- .maestro/scripts/setup-ios-simulator.sh | 4 +- 3 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/e2e.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..1829ac6a --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,168 @@ +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: Install Maestro CLI + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "$HOME/.maestro/bin" >> $GITHUB_PATH + + - 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: Run E2E tests + run: yarn test:e2e:ios + + - name: Upload test artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-ios-artifacts + path: | + .maestro/screenshots/ + + 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" + 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: 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/ diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index 3c74de08..465e69eb 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -50,10 +50,11 @@ 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 @@ -63,7 +64,11 @@ 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 diff --git a/.maestro/scripts/setup-ios-simulator.sh b/.maestro/scripts/setup-ios-simulator.sh index 66f4cb74..a8ef0c69 100755 --- a/.maestro/scripts/setup-ios-simulator.sh +++ b/.maestro/scripts/setup-ios-simulator.sh @@ -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" From 7096b2ffeb3f6885c123a855da56ad284a0a67a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 26 Mar 2026 17:14:44 +0100 Subject: [PATCH 02/22] fix: maestro version --- .maestro/scripts/run-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/scripts/run-tests.sh b/.maestro/scripts/run-tests.sh index d32d24dc..5402a209 100755 --- a/.maestro/scripts/run-tests.sh +++ b/.maestro/scripts/run-tests.sh @@ -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 From 466c2c5bdaacfa2b8c2a70920461b15b2f9874f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 26 Mar 2026 17:18:31 +0100 Subject: [PATCH 03/22] fix: emulator start for CI --- .maestro/scripts/run-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/scripts/run-tests.sh b/.maestro/scripts/run-tests.sh index 5402a209..97929054 100755 --- a/.maestro/scripts/run-tests.sh +++ b/.maestro/scripts/run-tests.sh @@ -58,7 +58,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 From f72b8a211adbf255d93321f122405d60e1306d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 26 Mar 2026 17:38:57 +0100 Subject: [PATCH 04/22] fix: run maestro server --- .github/workflows/e2e.yml | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 1829ac6a..4bd7efbd 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -71,31 +71,8 @@ jobs: curl -Ls "https://get.maestro.mobile.dev" | bash echo "$HOME/.maestro/bin" >> $GITHUB_PATH - - 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: Start Metro + run: yarn example start & - name: Run E2E tests run: yarn test:e2e:ios @@ -135,6 +112,7 @@ jobs: - 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 @@ -154,6 +132,9 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- + - name: Start Metro + run: yarn example start & + - name: Run E2E tests run: yarn test:e2e:android env: From b12033974aee5e0c292157fae8ee1c581e81412f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 26 Mar 2026 17:44:07 +0100 Subject: [PATCH 05/22] fix: andorid e2e --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4bd7efbd..a2d27369 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -112,7 +112,7 @@ jobs: - 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" + $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "emulator" "platform-tools" "skins;android-36" echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> $GITHUB_PATH echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH echo "$ANDROID_HOME/emulator" >> $GITHUB_PATH From a4de3d3c9b35d5f489d8cb25ac556930f75565da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 26 Mar 2026 18:04:21 +0100 Subject: [PATCH 06/22] fix: restore cocoapods setup --- .github/workflows/e2e.yml | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a2d27369..f9957436 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -66,6 +66,33 @@ jobs: with: xcode-version: latest-stable + - name: Restore cocoapods + if: env.turbo_cache_hit != 1 + 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: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' + run: | + cd apps/example/ios + pod install + env: + NO_FLIPPER: 1 + + - name: Cache cocoapods + if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: | + **/ios/Pods + key: ${{ steps.cocoapods-cache.outputs.cache-key }} + - name: Install Maestro CLI run: | curl -Ls "https://get.maestro.mobile.dev" | bash @@ -112,7 +139,7 @@ jobs: - 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" "skins;android-36" + $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 From 912a537352493a918409474397941a2f43bfa36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 27 Mar 2026 08:26:31 +0100 Subject: [PATCH 07/22] fix: cache derived data --- .github/workflows/e2e.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f9957436..13850eaf 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -67,7 +67,6 @@ jobs: xcode-version: latest-stable - name: Restore cocoapods - if: env.turbo_cache_hit != 1 id: cocoapods-cache uses: actions/cache/restore@v4 with: @@ -78,7 +77,7 @@ jobs: ${{ runner.os }}-cocoapods- - name: Install cocoapods - if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' + if: steps.cocoapods-cache.outputs.cache-hit != 'true' run: | cd apps/example/ios pod install @@ -86,13 +85,22 @@ jobs: NO_FLIPPER: 1 - name: Cache cocoapods - if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' + 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 @@ -104,6 +112,13 @@ jobs: - 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 From 14cd6c48f759b843f187b02d470bff9a96de71fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 27 Mar 2026 09:41:12 +0100 Subject: [PATCH 08/22] fix: print available devices --- .github/workflows/e2e.yml | 157 +++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 77 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 13850eaf..168a4146 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -49,83 +49,83 @@ jobs: - '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/ + # 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/ e2e-android: needs: [changes] @@ -159,6 +159,9 @@ jobs: echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH echo "$ANDROID_HOME/emulator" >> $GITHUB_PATH + - name: List available Android device definitions + run: $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager list device -c + - name: Install Maestro CLI run: | curl -Ls "https://get.maestro.mobile.dev" | bash From d6fd01746af239bc8130fd01db69cb963a6ba5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 27 Mar 2026 10:30:25 +0100 Subject: [PATCH 09/22] fix: set pixel_9 screen dimensions --- .maestro/scripts/setup-android-emulator.sh | 34 +++++++++++++++++----- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index 465e69eb..646dc608 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -34,18 +34,28 @@ if ! sdkmanager --list_installed 2>/dev/null | grep -q "system-images;android-${ sdkmanager "$SYSTEM_IMAGE" fi +# Pixel 9 screen specs injected into config.ini only when the native profile isn't available. +PIXEL_9_LCD_WIDTH="1080" +PIXEL_9_LCD_HEIGHT="2424" +PIXEL_9_LCD_DENSITY="422" + +AVD_DEVICE_PROFILE="$DEVICE_ID" +PATCH_PIXEL9_DIMENSIONS="" if ! avdmanager list device -c | grep -qx "$DEVICE_ID"; then - echo "Error: Device definition '$DEVICE_ID' not found." - exit 1 + AVD_DEVICE_PROFILE="pixel_7" + PATCH_PIXEL9_DIMENSIONS="true" + if ! avdmanager list device -c | grep -qx "$AVD_DEVICE_PROFILE"; then + echo "Error: Neither '$DEVICE_ID' nor fallback '$AVD_DEVICE_PROFILE' device definition found." + exit 1 + fi + echo "Warning: '$DEVICE_ID' not found, using '$AVD_DEVICE_PROFILE' as base and patching Pixel 9 screen dimensions." 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 "$AVD_DEVICE_PROFILE" --package "$SYSTEM_IMAGE") + [ -z "$PATCH_PIXEL9_DIMENSIONS" ] && CREATE_CMD+=(--skin "$DEVICE_ID") + echo "no" | "${CREATE_CMD[@]}" fi AVD_CONFIG="$HOME/.android/avd/${AVD_NAME}.avd/config.ini" @@ -54,6 +64,16 @@ if [ -f "$AVD_CONFIG" ]; then grep -q "^hw.keyboard=" "$AVD_CONFIG" || echo "hw.keyboard=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" + # Only patch screen dimensions when using the fallback profile; the native pixel_9 + # profile already sets the correct values via the skin. + if [ -n "$PATCH_PIXEL9_DIMENSIONS" ]; then + sed -i.bak "s/^hw\.lcd\.width=.*/hw.lcd.width=${PIXEL_9_LCD_WIDTH}/" "$AVD_CONFIG" + grep -q "^hw.lcd.width=" "$AVD_CONFIG" || echo "hw.lcd.width=${PIXEL_9_LCD_WIDTH}" >> "$AVD_CONFIG" + sed -i.bak "s/^hw\.lcd\.height=.*/hw.lcd.height=${PIXEL_9_LCD_HEIGHT}/" "$AVD_CONFIG" + grep -q "^hw.lcd.height=" "$AVD_CONFIG" || echo "hw.lcd.height=${PIXEL_9_LCD_HEIGHT}" >> "$AVD_CONFIG" + sed -i.bak "s/^hw\.lcd\.density=.*/hw.lcd.density=${PIXEL_9_LCD_DENSITY}/" "$AVD_CONFIG" + grep -q "^hw.lcd.density=" "$AVD_CONFIG" || echo "hw.lcd.density=${PIXEL_9_LCD_DENSITY}" >> "$AVD_CONFIG" + fi rm -f "$AVD_CONFIG.bak" fi From 442c7c6fe7f6754a762a9336595eeaeffb687d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 27 Mar 2026 10:38:19 +0100 Subject: [PATCH 10/22] fix: increase timeout fo android emulator --- .maestro/scripts/setup-android-emulator.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index 646dc608..2c818049 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -91,7 +91,7 @@ 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 +if ! timeout 300 adb -s "$SERIAL" wait-for-device; then echo "Error: Emulator did not connect to ADB after 120s." exit 1 fi From 84d08e209d2e9d81b78995247ed32861eae2ce26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 27 Mar 2026 10:47:24 +0100 Subject: [PATCH 11/22] fix: add emulator logs to file --- .github/workflows/e2e.yml | 3 --- .maestro/scripts/setup-android-emulator.sh | 7 ++++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 168a4146..e717a255 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -159,9 +159,6 @@ jobs: echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH echo "$ANDROID_HOME/emulator" >> $GITHUB_PATH - - name: List available Android device definitions - run: $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager list device -c - - name: Install Maestro CLI run: | curl -Ls "https://get.maestro.mobile.dev" | bash diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index 2c818049..bca0e9f3 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -88,7 +88,12 @@ 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 & + +if [ -n "${CI:-}" ]; then + emulator "${EMULATOR_ARGS[@]}" > /tmp/emulator.log 2>&1 & +else + emulator "${EMULATOR_ARGS[@]}" > /dev/null 2>&1 & +fi echo "Waiting for emulator ($SERIAL) to connect to ADB..." if ! timeout 300 adb -s "$SERIAL" wait-for-device; then From c33f579ea52b8de344d66171f24126308512fcaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 27 Mar 2026 11:01:20 +0100 Subject: [PATCH 12/22] fix: reduce timeout --- .github/workflows/e2e.yml | 1 + .maestro/scripts/setup-android-emulator.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e717a255..0b81c686 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -189,3 +189,4 @@ jobs: name: e2e-android-artifacts path: | .maestro/screenshots/ + /tmp/emulator.log diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index bca0e9f3..e137c5b8 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -96,7 +96,7 @@ else fi echo "Waiting for emulator ($SERIAL) to connect to ADB..." -if ! timeout 300 adb -s "$SERIAL" wait-for-device; then +if ! timeout 120 adb -s "$SERIAL" wait-for-device; then echo "Error: Emulator did not connect to ADB after 120s." exit 1 fi From f8096fea83f2d2a54343a123d27084fe17f3dd11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 27 Mar 2026 11:07:38 +0100 Subject: [PATCH 13/22] fix: avd set --- .maestro/scripts/setup-android-emulator.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index e137c5b8..aa781028 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -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." From 5bcedbe37a0f322bd87c03c081b0d116b7318f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 27 Mar 2026 11:20:11 +0100 Subject: [PATCH 14/22] fix: pixel density --- .maestro/scripts/setup-android-emulator.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index aa781028..b6789c59 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -42,7 +42,7 @@ fi # Pixel 9 screen specs injected into config.ini only when the native profile isn't available. PIXEL_9_LCD_WIDTH="1080" PIXEL_9_LCD_HEIGHT="2424" -PIXEL_9_LCD_DENSITY="422" +PIXEL_9_LCD_DENSITY="420" AVD_DEVICE_PROFILE="$DEVICE_ID" PATCH_PIXEL9_DIMENSIONS="" From d9ed9b9dc18969a12d0ea8ea9ac3bec53befdcc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 27 Mar 2026 13:19:20 +0100 Subject: [PATCH 15/22] fix: run android tests on pixel_7 --- .../android/empty_element_parsing.png | Bin 9861 -> 9860 bytes .../android/paragraph_styles_no_crash.png | Bin 38100 -> 38099 bytes .../scrolling_paragraph_styles_top.png | Bin 25708 -> 25708 bytes .maestro/scripts/setup-android-emulator.sh | 38 +++++------------- CONTRIBUTING.md | 2 +- 5 files changed, 10 insertions(+), 30 deletions(-) diff --git a/.maestro/screenshots/android/empty_element_parsing.png b/.maestro/screenshots/android/empty_element_parsing.png index 7e28ad4ea19824b6e32b9e29706bbf3e05066260..04053f6aa94d65a9936c9edbef9005cc4f496b56 100644 GIT binary patch literal 9860 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 Zo$xvF{B|t|fJ+3i*kiprb=S#j{{!Xq;h+Ej literal 9861 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 Uxqfxyyjz>Cm7`_N6~DWG0i8ZaJOBUy delta 2617 zcmZ9OdpwhUAIGPILvqWFtUHII5=!aLStKN4?&Oq1OsLG9GHT>fL=KVLsT|Uc$(W6e zmNA4JLhfPqFdK5VP_dTD_FSIl^?F|2&wtnJdR@Qo_xt*MKcDyS8p7@v#_o8k1F%-3 z)x$m+q^qdM{`s$-OeIxW<-3z_MeLvtlanQ&rvp-IYoE(Nf6ssGQx5snS1uRYapdrk z2S;}KK7}Tn-&y)}_n-Uz-1i%-$n_PBUOmsQyo%y9HJ~``oRxOp#g;~oMocg_cztVF z({`P|TF)CZx%Y$VQN@eJAAmq0acUCh)Xl&L$mCv9_DW>yqqUuoIOzl~Y+gn9-P+R9($CM&);8r6Y5DKJEW9WY zSEhp|n!Ucg4c+{rwD!T6D4m#);IxaE?D+NTSHGxNT^k~hR{b{gyyMlYr6gK+rkXz)880S1GF=n* zHc81iazPfgG_3J+<$WW@Q24O^2WbeTS!Tpop$#pjmd5YQ!tXc+EIaZh@@)myH`suw z=_X+60#p{$)rs9U;vA*4(@O{E#UPLgaSk$q)1rS&IFX?z0P07|O;^ADi2 zvvY1{W|(0{g&{1U)i|6}QrR4Ufu$QI%4@k)zz2$qU%`q_aLbCa0_WNG=0>0iA~X|F z!n+>)kJVVTq2(o%n_Ell7Df9_%tkngGmDMptt>ML#e(rNQwKY{pg>~%$Cj5befaB( z;g8iRk<0vL@$~-jr#DmUDs~b-lD1ITw6|}WoOIDV9(_ng!x)oJsSU>tmmM4C^|}V5cJ*4 zJE}M){j+z$4dH|lV*mC4g(PTsdm9>DkhVd@!qa9Mb_*Z`MPFt$@)l^No4i%;=9q@= z$G%&MSSWM^R_`4K&irvxSDpx|vZ~ANE>JBeH3E+LtbPgOiuymql zNqughm_R1p*?Xfaoz7*qRopvb0VMr$^2CX3dF`dv;fQg6%{#rpd3tu5anT{aeAw3S zcQqVyb~O-1GQ2s9zSKrxCd;b((PTBL+|>zhSBip%q<#o{xED*8zWJdrQ{LgMt}b%? z&6Ny2a76{yZ_wPp2?TN5sHs~WU*|?5p2vf#HYr{V3c{`KFjr|O`uF-33$yloV#jY2 zY+**66Nw}OmZ6*WNUbEv%%=(5?(S|dAU!F$;o;#C)c3O`WD13XVBlBS*Qw}x9yGYo zeSV$tX-!SrNRM!2SXeFY7PhI$Q-M4MRW*4;tMF!6zMVkCJeA(3>2GrufI4K?ZVS`ue-F4E-NX!K1riS z^C(6ey;5u9)=z#rimpyqF~f71p?hUswA~60#;@4h*~w9rL2oVE{a=^by`3|z zZr%ry4G4|Cem0G0ezC1 zIz$DgUSD5tbWlpg)H9L^GT{6{4s#(RttU zPOrwuo?yudIIBtM?&zqBrs}Au#Ljff&=AUSJG-=n`h3GX<@gY$j$3=hhT^(j%?r1d zWAgu^icS9jFH6v61D%(F%xsaJqHjoBsi8qrvwLB3EwXW!9&P}<5qM)Q2m~I8AU8wl1W%^o;~O=Jw2F%3tmw75 zen;SL%y`N_#5WV*J)=GZqWV3th0mXBG$JqA*wEBKqB#DiB(T=6Php^qM+UGZR$~;0|5_t@CbKcLQQN5M~9#?yWxB4~*x^4;x zq$Ei*6OmX_mo?l#sz>lJ5dx9Ru(33E`h%n!mntW#iady3m$|q7gJxp|v&3KUdH8Rd CkXS$f diff --git a/.maestro/screenshots/android/scrolling_paragraph_styles_top.png b/.maestro/screenshots/android/scrolling_paragraph_styles_top.png index 3b946339fbdb9ba08a24cb5c35862b618ac59b93..f46c61242e02e761afd6eaabe685f60efd49921d 100644 GIT binary patch delta 43 ycmaEJg7M7>#tqecOrPa9*YahnPJX2*y7{khyn%r7%OfHTK;Y@>=d#Wzp$Pz&`w)=; delta 43 ycmaEJg7M7>#tqecOn>KZuI0;CWqxKqYx8U4LIZ&#FK@Rp0D-5gpUXO@geCyF?-El0 diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index b6789c59..ec69b367 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -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" @@ -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}" @@ -39,28 +39,18 @@ if ! sdkmanager --list_installed 2>/dev/null | grep -q "system-images;android-${ sdkmanager "$SYSTEM_IMAGE" fi -# Pixel 9 screen specs injected into config.ini only when the native profile isn't available. -PIXEL_9_LCD_WIDTH="1080" -PIXEL_9_LCD_HEIGHT="2424" -PIXEL_9_LCD_DENSITY="420" - -AVD_DEVICE_PROFILE="$DEVICE_ID" -PATCH_PIXEL9_DIMENSIONS="" if ! avdmanager list device -c | grep -qx "$DEVICE_ID"; then - AVD_DEVICE_PROFILE="pixel_7" - PATCH_PIXEL9_DIMENSIONS="true" - if ! avdmanager list device -c | grep -qx "$AVD_DEVICE_PROFILE"; then - echo "Error: Neither '$DEVICE_ID' nor fallback '$AVD_DEVICE_PROFILE' device definition found." - exit 1 - fi - echo "Warning: '$DEVICE_ID' not found, using '$AVD_DEVICE_PROFILE' as base and patching Pixel 9 screen dimensions." + echo "Error: Device definition '$DEVICE_ID' not found." + exit 1 fi if ! avdmanager list avd -c | grep -qx "${AVD_NAME}"; then echo "Creating AVD '$AVD_NAME'..." - CREATE_CMD=(avdmanager create avd --name "$AVD_NAME" --device "$AVD_DEVICE_PROFILE" --package "$SYSTEM_IMAGE") - [ -z "$PATCH_PIXEL9_DIMENSIONS" ] && CREATE_CMD+=(--skin "$DEVICE_ID") - echo "no" | "${CREATE_CMD[@]}" + echo "no" | avdmanager create avd \ + --name "$AVD_NAME" \ + --device "$DEVICE_ID" \ + --package "$SYSTEM_IMAGE" \ + --skin "$DEVICE_ID" fi AVD_CONFIG="$HOME/.android/avd/${AVD_NAME}.avd/config.ini" @@ -69,16 +59,6 @@ if [ -f "$AVD_CONFIG" ]; then grep -q "^hw.keyboard=" "$AVD_CONFIG" || echo "hw.keyboard=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" - # Only patch screen dimensions when using the fallback profile; the native pixel_9 - # profile already sets the correct values via the skin. - if [ -n "$PATCH_PIXEL9_DIMENSIONS" ]; then - sed -i.bak "s/^hw\.lcd\.width=.*/hw.lcd.width=${PIXEL_9_LCD_WIDTH}/" "$AVD_CONFIG" - grep -q "^hw.lcd.width=" "$AVD_CONFIG" || echo "hw.lcd.width=${PIXEL_9_LCD_WIDTH}" >> "$AVD_CONFIG" - sed -i.bak "s/^hw\.lcd\.height=.*/hw.lcd.height=${PIXEL_9_LCD_HEIGHT}/" "$AVD_CONFIG" - grep -q "^hw.lcd.height=" "$AVD_CONFIG" || echo "hw.lcd.height=${PIXEL_9_LCD_HEIGHT}" >> "$AVD_CONFIG" - sed -i.bak "s/^hw\.lcd\.density=.*/hw.lcd.density=${PIXEL_9_LCD_DENSITY}/" "$AVD_CONFIG" - grep -q "^hw.lcd.density=" "$AVD_CONFIG" || echo "hw.lcd.density=${PIXEL_9_LCD_DENSITY}" >> "$AVD_CONFIG" - fi rm -f "$AVD_CONFIG.bak" fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59808d07..2aac522d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 From 39ae9ed1a05c84ca8d5db5171da57e2378e7d954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 27 Mar 2026 13:26:05 +0100 Subject: [PATCH 16/22] fix: remove --skin for CI --- .maestro/scripts/setup-android-emulator.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index ec69b367..79c18315 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -46,11 +46,11 @@ 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" From ddfeadea2981ba9e6cc53adeecb0b5121b51bcdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 30 Mar 2026 12:49:39 +0200 Subject: [PATCH 17/22] fix: make e2e tests run as nightly --- .github/workflows/e2e.yml | 199 ++++++++------------- .maestro/scripts/setup-android-emulator.sh | 6 +- 2 files changed, 77 insertions(+), 128 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0b81c686..d60a08f8 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,135 +1,89 @@ name: E2E Tests on: - push: - branches: - - main - pull_request: - branches: - - main - merge_group: - types: - - checks_requested + # For nightly runs + schedule: + - cron: '30 0 * * *' + # For manual runs + workflow_dispatch: jobs: - changes: - runs-on: ubuntu-latest - outputs: - android: ${{ steps.filter.outputs.android }} - ios: ${{ steps.filter.outputs.ios }} + e2e-ios: + runs-on: macos-latest + timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v4 - - name: Check file changes - uses: dorny/paths-filter@v3 - id: filter + - 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: - 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/ + 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/**/*_diff.png e2e-android: - needs: [changes] - if: needs.changes.outputs.android == 'true' runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -188,5 +142,4 @@ jobs: with: name: e2e-android-artifacts path: | - .maestro/screenshots/ - /tmp/emulator.log + .maestro/screenshots/**/*_diff.png diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index 79c18315..3b8313a2 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -74,11 +74,7 @@ if [ -n "${CI:-}" ]; then EMULATOR_ARGS+=(-no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim) fi -if [ -n "${CI:-}" ]; then - emulator "${EMULATOR_ARGS[@]}" > /tmp/emulator.log 2>&1 & -else - emulator "${EMULATOR_ARGS[@]}" > /dev/null 2>&1 & -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 From 89348e935c75e5e49e4b34b0d547645998207e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 30 Mar 2026 13:00:31 +0200 Subject: [PATCH 18/22] fix: restore CI run on PR --- .github/workflows/e2e.yml | 56 +++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d60a08f8..dd562bf9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,13 +1,57 @@ name: E2E Tests on: - # For nightly runs - schedule: - - cron: '30 0 * * *' - # For manual runs - workflow_dispatch: + 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: @@ -84,6 +128,8 @@ jobs: .maestro/screenshots/**/*_diff.png e2e-android: + needs: [changes] + if: needs.changes.outputs.android == 'true' runs-on: ubuntu-latest timeout-minutes: 60 steps: From f729733e491700ef0a4cbe039201646385f238a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 30 Mar 2026 13:06:27 +0200 Subject: [PATCH 19/22] fix: remove passed e2e tests on ANdroid --- .../flows/codeblock_no_link_detection.yaml | 24 ------ .maestro/flows/codeblock_style_blocking.yaml | 50 ------------- .../flows/conflicting_paragraph_merge.yaml | 59 --------------- .maestro/flows/core_controls_smoke.yaml | 56 -------------- .maestro/flows/empty_element_parsing.yaml | 34 --------- .maestro/flows/initial_html_parsing.yaml | 22 ------ .../inline_styles_survive_block_toggle.yaml | 29 -------- .maestro/flows/inline_styles_visual.yaml | 50 ------------- .maestro/flows/link_not_extended.yaml | 34 --------- .maestro/flows/list_newline_insertion.yaml | 44 ----------- ...sing_handles_single_quoted_attributes.yaml | 19 ----- .maestro/flows/ordered_list_renumbering.yaml | 43 ----------- .maestro/flows/paragraph_style_removal.yaml | 35 --------- .maestro/flows/paragraph_style_toggle.yaml | 34 --------- .../flows/paragraph_styles_blocks_visual.yaml | 34 --------- .../paragraph_styles_headings_visual.yaml | 46 ------------ .maestro/flows/placeholder_visual.yaml | 11 --- .maestro/flows/scrolling_after_typing.yaml | 63 ---------------- .maestro/flows/scrolling_set_value.yaml | 64 ---------------- .../scrolling_with_paragraph_styles.yaml | 74 ------------------- 20 files changed, 825 deletions(-) delete mode 100644 .maestro/flows/codeblock_no_link_detection.yaml delete mode 100644 .maestro/flows/codeblock_style_blocking.yaml delete mode 100644 .maestro/flows/conflicting_paragraph_merge.yaml delete mode 100644 .maestro/flows/core_controls_smoke.yaml delete mode 100644 .maestro/flows/empty_element_parsing.yaml delete mode 100644 .maestro/flows/initial_html_parsing.yaml delete mode 100644 .maestro/flows/inline_styles_survive_block_toggle.yaml delete mode 100644 .maestro/flows/inline_styles_visual.yaml delete mode 100644 .maestro/flows/link_not_extended.yaml delete mode 100644 .maestro/flows/list_newline_insertion.yaml delete mode 100644 .maestro/flows/mention_parsing_handles_single_quoted_attributes.yaml delete mode 100644 .maestro/flows/ordered_list_renumbering.yaml delete mode 100644 .maestro/flows/paragraph_style_removal.yaml delete mode 100644 .maestro/flows/paragraph_style_toggle.yaml delete mode 100644 .maestro/flows/paragraph_styles_blocks_visual.yaml delete mode 100644 .maestro/flows/paragraph_styles_headings_visual.yaml delete mode 100644 .maestro/flows/placeholder_visual.yaml delete mode 100644 .maestro/flows/scrolling_after_typing.yaml delete mode 100644 .maestro/flows/scrolling_set_value.yaml delete mode 100644 .maestro/flows/scrolling_with_paragraph_styles.yaml diff --git a/.maestro/flows/codeblock_no_link_detection.yaml b/.maestro/flows/codeblock_no_link_detection.yaml deleted file mode 100644 index 58591351..00000000 --- a/.maestro/flows/codeblock_no_link_detection.yaml +++ /dev/null @@ -1,24 +0,0 @@ -appId: swmansion.enriched.example ---- -# PR #257 - fix: handle style conflicts during automatic links detection -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- tapOn: - id: "toolbar-code-block" - -- inputText: "Hello example.com" - -- assertVisible: - id: "toolbar-link" - enabled: false - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "codeblock_no_link_detection" diff --git a/.maestro/flows/codeblock_style_blocking.yaml b/.maestro/flows/codeblock_style_blocking.yaml deleted file mode 100644 index 962b2f76..00000000 --- a/.maestro/flows/codeblock_style_blocking.yaml +++ /dev/null @@ -1,50 +0,0 @@ -appId: swmansion.enriched.example ---- -# Verifies that inside a code block, inline styles and rich features are -# blocked in the toolbar. Code blocks do not support inline formatting, -# automatic link detection, or mention insertion. -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- tapOn: - id: "toolbar-code-block" - -- inputText: "ABC example.com" - -- assertVisible: - id: "toolbar-bold" - enabled: false - -- assertVisible: - id: "toolbar-italic" - enabled: false - -- assertVisible: - id: "toolbar-underline" - enabled: false - -- assertVisible: - id: "toolbar-strikethrough" - enabled: false - -- assertVisible: - id: "toolbar-inline-code" - enabled: false - -- assertVisible: - id: "toolbar-link" - enabled: false - -- assertVisible: - id: "toolbar-mention" - enabled: false - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "codeblock_style_blocking" diff --git a/.maestro/flows/conflicting_paragraph_merge.yaml b/.maestro/flows/conflicting_paragraph_merge.yaml deleted file mode 100644 index 920e7388..00000000 --- a/.maestro/flows/conflicting_paragraph_merge.yaml +++ /dev/null @@ -1,59 +0,0 @@ -appId: swmansion.enriched.example ---- -# PR #306 - fix: handle conflicts between paragraphs -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- tapOn: - id: "toolbar-code-block" -- inputText: "Code text" -- pressKey: Enter - -- tapOn: - id: "toolbar-code-block" -- tapOn: - id: "toolbar-heading-1" -- inputText: "H1" - -# Position cursor at start of H1 line -- tapOn: - id: "editor-input" - point: "5%,75%" - -- pressKey: Backspace - -- runFlow: - when: - platform: "android" - commands: - - pressKey: Backspace -- inputText: " " - -- tapOn: - id: "editor-input" - point: "75%, 50%" - -- pressKey: Enter -- tapOn: - id: "toolbar-code-block" - -- tapOn: - id: "toolbar-inline-code" -- inputText: "Inline code" - -- tapOn: - id: "editor-input" - point: "5%, 75%" - -- pressKey: Backspace -- inputText: " " - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "conflicting_paragraph_merge" diff --git a/.maestro/flows/core_controls_smoke.yaml b/.maestro/flows/core_controls_smoke.yaml deleted file mode 100644 index 6e229ac6..00000000 --- a/.maestro/flows/core_controls_smoke.yaml +++ /dev/null @@ -1,56 +0,0 @@ -appId: swmansion.enriched.example ---- -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- assertVisible: - id: "editor-input" - -- assertVisible: - id: "focus-button" -- assertVisible: - id: "blur-button" -- assertVisible: - id: "set-value-button" - - -- assertVisible: - id: "toolbar-bold" -- assertVisible: - id: "toolbar-italic" -- assertVisible: - id: "toolbar-underline" -- assertVisible: - id: "toolbar-strikethrough" -- assertVisible: - id: "toolbar-inline-code" -- assertVisible: - id: "toolbar-heading-1" -- assertVisible: - id: "toolbar-heading-2" -- assertVisible: - id: "toolbar-heading-3" -- assertVisible: - id: "toolbar-heading-4" -- assertVisible: - id: "toolbar-heading-5" -- assertVisible: - id: "toolbar-heading-6" -- assertVisible: - id: "toolbar-quote" -- assertVisible: - id: "toolbar-code-block" -- assertVisible: - id: "toolbar-image" -- assertVisible: - id: "toolbar-link" -- assertVisible: - id: "toolbar-mention" -- assertVisible: - id: "toolbar-unordered-list" -- assertVisible: - id: "toolbar-ordered-list" -- assertVisible: - id: "toolbar-checkbox-list" diff --git a/.maestro/flows/empty_element_parsing.yaml b/.maestro/flows/empty_element_parsing.yaml deleted file mode 100644 index 4429bf8e..00000000 --- a/.maestro/flows/empty_element_parsing.yaml +++ /dev/null @@ -1,34 +0,0 @@ -appId: swmansion.enriched.example -tags: - - android-only ---- -# PR #284 - fix: parsing empty elements -- launchApp -- tapOn: - id: "toggle-screen-button" - -- runFlow: - file: "../subflows/set_editor_value.yaml" - env: - VALUE: > - -

A

-

B

-

A

-

B

-
    -
  • A
  • -
  • B
  • -
  • C
  • -
  • -
-
- - -- tapOn: - id: "size-max-button" - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "empty_element_parsing" diff --git a/.maestro/flows/initial_html_parsing.yaml b/.maestro/flows/initial_html_parsing.yaml deleted file mode 100644 index 868867d6..00000000 --- a/.maestro/flows/initial_html_parsing.yaml +++ /dev/null @@ -1,22 +0,0 @@ -appId: swmansion.enriched.example ---- -# PR #462 - fix(android): parsing initial HTML -# Verifies that setting an HTML defaultValue with headings renders correctly -# without duplicate or extra tags being inserted. -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- runFlow: - file: "../subflows/set_editor_value.yaml" - env: - VALUE: "

Hello

World

!!!

" - -- tapOn: - id: "size-max-button" - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "initial_html_parsing" \ No newline at end of file diff --git a/.maestro/flows/inline_styles_survive_block_toggle.yaml b/.maestro/flows/inline_styles_survive_block_toggle.yaml deleted file mode 100644 index b2e5bfb9..00000000 --- a/.maestro/flows/inline_styles_survive_block_toggle.yaml +++ /dev/null @@ -1,29 +0,0 @@ -appId: swmansion.enriched.example ---- -# PR #370 - fix: reapplying spans -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- tapOn: - id: "toolbar-bold" -- inputText: "Bold" - -- tapOn: - id: "toolbar-bold" -- inputText: " regular" - -- tapOn: - id: "toolbar-quote" - -- tapOn: - id: "toolbar-quote" - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "inline_styles_survive_block_toggle" diff --git a/.maestro/flows/inline_styles_visual.yaml b/.maestro/flows/inline_styles_visual.yaml deleted file mode 100644 index f8f2719a..00000000 --- a/.maestro/flows/inline_styles_visual.yaml +++ /dev/null @@ -1,50 +0,0 @@ -appId: swmansion.enriched.example ---- -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- inputText: "Plain " - -- tapOn: - id: "toolbar-bold" -- inputText: "bold" -- tapOn: - id: "toolbar-bold" -- inputText: " " - -- tapOn: - id: "toolbar-italic" -- inputText: "italic" -- tapOn: - id: "toolbar-italic" -- inputText: " " - -- tapOn: - id: "toolbar-underline" -- inputText: "underline" -- tapOn: - id: "toolbar-underline" -- inputText: " " - -- tapOn: - id: "toolbar-strikethrough" -- inputText: "strike-through" -- tapOn: - id: "toolbar-strikethrough" -- inputText: " " - -- tapOn: - id: "toolbar-inline-code" -- inputText: "code" -- tapOn: - id: "toolbar-inline-code" - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "inline_styles" diff --git a/.maestro/flows/link_not_extended.yaml b/.maestro/flows/link_not_extended.yaml deleted file mode 100644 index b557b5dd..00000000 --- a/.maestro/flows/link_not_extended.yaml +++ /dev/null @@ -1,34 +0,0 @@ -appId: swmansion.enriched.example -tags: - - ios-only ---- -# PR #421 - fix(iOS): prevent extending link styles -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- inputText: "Hello " - -- runFlow: - file: "../subflows/insert_link.yaml" - env: - LINK_TEXT: "test" - LINK_URL: "https://swmansion.com" - -- tapOn: - id: "editor-input" - point: "5%, 50%" -- tapOn: - id: "editor-input" - point: "95%, 50%" - -- inputText: "ing" - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "link_not_extended" diff --git a/.maestro/flows/list_newline_insertion.yaml b/.maestro/flows/list_newline_insertion.yaml deleted file mode 100644 index 79e0dfd1..00000000 --- a/.maestro/flows/list_newline_insertion.yaml +++ /dev/null @@ -1,44 +0,0 @@ -appId: swmansion.enriched.example ---- -# PR #401 - fix(Android): newline insertion in lists -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- tapOn: - id: "toolbar-ordered-list" - -- runFlow: - when: - platform: android - commands: - - inputText: "XXXXXXXXXXXXXXXXXXXXXXXX" - - tapOn: - id: "editor-input" - point: "50%,50%" -# iOS places the cursor on word boundaries by default. Precisely positioning -# the cursor would require a press-and-drag gesture, which Maestro does not -# support. As a workaround we type two words, tap between them (snapping to -# the word boundary/space), then delete the space so we end up with a -# continuous word and the cursor in the middle. -- runFlow: - when: - platform: ios - commands: - - inputText: "XXXXXXXXXXX XXXXXXXXXXXXX" - - tapOn: - id: "editor-input" - point: "50%,50%" - - pressKey: Backspace - - -- pressKey: Enter - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "list_newline_insertion" diff --git a/.maestro/flows/mention_parsing_handles_single_quoted_attributes.yaml b/.maestro/flows/mention_parsing_handles_single_quoted_attributes.yaml deleted file mode 100644 index 83d4df74..00000000 --- a/.maestro/flows/mention_parsing_handles_single_quoted_attributes.yaml +++ /dev/null @@ -1,19 +0,0 @@ -appId: swmansion.enriched.example ---- -# "PR #405 - fix(ios): mention parsing" -# Verifies that mentions with single-quoted HTML attributes are parsed and styled correctly. - -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- runFlow: - file: "../subflows/set_editor_value.yaml" - env: - VALUE: "@John Doe" - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "mention_parsing_single_quoted_attributes" diff --git a/.maestro/flows/ordered_list_renumbering.yaml b/.maestro/flows/ordered_list_renumbering.yaml deleted file mode 100644 index 9865c269..00000000 --- a/.maestro/flows/ordered_list_renumbering.yaml +++ /dev/null @@ -1,43 +0,0 @@ -appId: swmansion.enriched.example ---- -# PR #359 - fix: android span watcher -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- tapOn: - id: "toolbar-ordered-list" - -- inputText: "A" -- pressKey: Enter -- inputText: "B" -- pressKey: Enter -- inputText: "C" - -# Tap on the second line to position cursor there -- tapOn: - id: "editor-input" - point: "50%,50%" - -- pressKey: backspace -- pressKey: backspace - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "ordered_list_renumbering_after_emptying_second_line" - -- tapOn: - id: "editor-input" - point: "50%,50%" - -- pressKey: backspace - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "ordered_list_renumbering_after_deleting_second_line" diff --git a/.maestro/flows/paragraph_style_removal.yaml b/.maestro/flows/paragraph_style_removal.yaml deleted file mode 100644 index c749a005..00000000 --- a/.maestro/flows/paragraph_style_removal.yaml +++ /dev/null @@ -1,35 +0,0 @@ -appId: swmansion.enriched.example ---- -# PR #430 - fix(Android): removing paragraph styles -# Verifies that backspacing text from a line in a code block does not strip -# paragraph styles from the remaining lines. -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- tapOn: - id: "toolbar-quote" - -- inputText: "A" -- pressKey: Enter -- inputText: "B" -- pressKey: Enter -- inputText: "C" - -# Tap on the second line -- tapOn: - id: "editor-input" - point: "40%,50%" - -# Clear second line -- pressKey: Backspace -- pressKey: Backspace - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "paragraph_style_after_removal" diff --git a/.maestro/flows/paragraph_style_toggle.yaml b/.maestro/flows/paragraph_style_toggle.yaml deleted file mode 100644 index 4e200abe..00000000 --- a/.maestro/flows/paragraph_style_toggle.yaml +++ /dev/null @@ -1,34 +0,0 @@ -appId: swmansion.enriched.example ---- -# PR #428 - fix(android): merging and splitting paragraph styles Verifies that -# toggling off a paragraph style on one line does not affect the style of -# adjacent lines -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- tapOn: - id: "toolbar-quote" - -- inputText: "1" -- pressKey: Enter -- inputText: "2" -- pressKey: Enter -- inputText: "3" - -# Tap on the second line -- tapOn: - id: "editor-input" - point: "40%,50%" - -- tapOn: - id: "toolbar-quote" - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "paragraph_style_toggle" diff --git a/.maestro/flows/paragraph_styles_blocks_visual.yaml b/.maestro/flows/paragraph_styles_blocks_visual.yaml deleted file mode 100644 index 1a437c12..00000000 --- a/.maestro/flows/paragraph_styles_blocks_visual.yaml +++ /dev/null @@ -1,34 +0,0 @@ -appId: swmansion.enriched.example ---- -# Visually validates rendering of block-level paragraph styles: -# blockquote and code block, each with multiple lines. -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- tapOn: - id: "toolbar-quote" -- inputText: "Quote 1" -- pressKey: Enter -- inputText: "Quote 2" -- pressKey: Enter -- tapOn: - id: "toolbar-quote" - -- inputText: "Normal paragraph" -- pressKey: Enter - -- tapOn: - id: "toolbar-code-block" -- inputText: "Code 1" -- pressKey: Enter -- inputText: "Code 2" - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "paragraph_styles_blocks" diff --git a/.maestro/flows/paragraph_styles_headings_visual.yaml b/.maestro/flows/paragraph_styles_headings_visual.yaml deleted file mode 100644 index 70c426fd..00000000 --- a/.maestro/flows/paragraph_styles_headings_visual.yaml +++ /dev/null @@ -1,46 +0,0 @@ -appId: swmansion.enriched.example ---- -# Visually validates rendering of all heading levels (H1–H6). -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- tapOn: - id: "toolbar-heading-1" -- inputText: "H1" -- pressKey: Enter - -- tapOn: - id: "toolbar-heading-2" -- inputText: "H2" -- pressKey: Enter - -- tapOn: - id: "toolbar-heading-3" -- inputText: "H3" -- pressKey: Enter - -- tapOn: - id: "toolbar-heading-4" -- inputText: "H4" -- pressKey: Enter - -- tapOn: - id: "toolbar-heading-5" -- inputText: "H5" -- pressKey: Enter - -- tapOn: - id: "toolbar-heading-6" -- inputText: "H6" -- tapOn: - id: "size-max-button" - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "paragraph_styles_headings" diff --git a/.maestro/flows/placeholder_visual.yaml b/.maestro/flows/placeholder_visual.yaml deleted file mode 100644 index 4861f20f..00000000 --- a/.maestro/flows/placeholder_visual.yaml +++ /dev/null @@ -1,11 +0,0 @@ -appId: swmansion.enriched.example ---- -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "initial_placeholder" diff --git a/.maestro/flows/scrolling_after_typing.yaml b/.maestro/flows/scrolling_after_typing.yaml deleted file mode 100644 index c9fa0b25..00000000 --- a/.maestro/flows/scrolling_after_typing.yaml +++ /dev/null @@ -1,63 +0,0 @@ -appId: swmansion.enriched.example -# Tests basic scrollability ---- - -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- inputText: "Line one" -- pressKey: Enter -- inputText: "Line two" -- pressKey: Enter -- inputText: "Line three" -- pressKey: Enter -- inputText: "Line four" -- pressKey: Enter -- inputText: "Line five" -- pressKey: Enter -- inputText: "Line six" -- pressKey: Enter -- inputText: "Line seven" -- pressKey: Enter -- inputText: "Line eight" -- pressKey: Enter -- inputText: "Line nine" -- pressKey: Enter -- inputText: "Line ten" - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "scroll_after_typing_long_content_bottom" - -- tapOn: - id: "focus-button" - -- swipe: - from: - id: "editor-input" - direction: DOWN - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "scroll_after_typing_long_content_top" - -- tapOn: - id: "editor-input" - point: "0%,0%" - -- swipe: - from: - id: "editor-input" - direction: UP - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "scroll_after_typing_long_content_bottom" diff --git a/.maestro/flows/scrolling_set_value.yaml b/.maestro/flows/scrolling_set_value.yaml deleted file mode 100644 index 80d38083..00000000 --- a/.maestro/flows/scrolling_set_value.yaml +++ /dev/null @@ -1,64 +0,0 @@ -appId: swmansion.enriched.example -# Tests scrollability when content is set via setValue ---- -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- runFlow: - file: "../subflows/set_editor_value.yaml" - env: - VALUE: >- - -

Line one

-

Line two

-

Line three

-

Line four

-

Line five

-

Line six

-

Line seven

-

Line eight

-

Line nine

-

Line ten

- - -# iOS starts at top - screenshot top, then swipe to bottom -- runFlow: - when: - platform: iOS - commands: - - runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "scrolling_set_value_top" - - tapOn: - id: "focus-button" - - swipe: - from: - id: "editor-input" - direction: UP - - runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "scrolling_set_value_bottom" - -# Android starts at bottom - screenshot bottom, then swipe to top -- runFlow: - when: - platform: Android - commands: - - runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "scrolling_set_value_bottom" - - tapOn: - id: "focus-button" - - swipe: - from: - id: "editor-input" - direction: DOWN - - runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "scrolling_set_value_top" diff --git a/.maestro/flows/scrolling_with_paragraph_styles.yaml b/.maestro/flows/scrolling_with_paragraph_styles.yaml deleted file mode 100644 index 96e987fe..00000000 --- a/.maestro/flows/scrolling_with_paragraph_styles.yaml +++ /dev/null @@ -1,74 +0,0 @@ -appId: swmansion.enriched.example -# Tests scrolling with mixed block formatting (lists, quotes) ---- -- launchApp - -- tapOn: - id: "toggle-screen-button" - -- tapOn: - id: "editor-input" - -- tapOn: - id: "toolbar-unordered-list" -- inputText: "Bullet item one" -- pressKey: Enter -- inputText: "Bullet item two" -- pressKey: Enter -- inputText: "Bullet item three" -- pressKey: Enter -- pressKey: Enter - -- tapOn: - id: "toolbar-quote" -- inputText: "Quoted line one" -- pressKey: Enter -- inputText: "Quoted line two" -- pressKey: Enter -- inputText: "Quoted line three" -- pressKey: Enter -- tapOn: - id: "toolbar-quote" - -- tapOn: - id: "toolbar-ordered-list" -- inputText: "Numbered item one" -- pressKey: Enter -- inputText: "Numbered item two" -- pressKey: Enter -- inputText: "Numbered item three" -- pressKey: Enter -- pressKey: Enter - -- pressKey: Backspace - -- inputText: "Plain text line one" -- pressKey: Enter -- inputText: "Plain text final" - -- swipe: - from: - id: "editor-input" - direction: DOWN - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "scrolling_paragraph_styles_top" - -- tapOn: - id: "focus-button" - -- tapOn: - id: "editor-input" - point: "0%,0%" - -- swipe: - from: - id: "editor-input" - direction: UP - -- runFlow: - file: "../subflows/capture_or_assert_screenshot.yaml" - env: - SCREENSHOT_NAME: "scrolling_paragraph_styles_bottom" From b5d30b134b6f0a05487015f4033aacffe9da46ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 30 Mar 2026 13:08:25 +0200 Subject: [PATCH 20/22] fix: add link not extended --- .maestro/flows/link_not_extended.yaml | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .maestro/flows/link_not_extended.yaml diff --git a/.maestro/flows/link_not_extended.yaml b/.maestro/flows/link_not_extended.yaml new file mode 100644 index 00000000..b557b5dd --- /dev/null +++ b/.maestro/flows/link_not_extended.yaml @@ -0,0 +1,34 @@ +appId: swmansion.enriched.example +tags: + - ios-only +--- +# PR #421 - fix(iOS): prevent extending link styles +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- inputText: "Hello " + +- runFlow: + file: "../subflows/insert_link.yaml" + env: + LINK_TEXT: "test" + LINK_URL: "https://swmansion.com" + +- tapOn: + id: "editor-input" + point: "5%, 50%" +- tapOn: + id: "editor-input" + point: "95%, 50%" + +- inputText: "ing" + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "link_not_extended" From 6dccf89a809f6f25b6399ba2670f4332be183b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 30 Mar 2026 13:48:41 +0200 Subject: [PATCH 21/22] fix: add exact path for diff pngs --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index dd562bf9..fb8a2d11 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -188,4 +188,4 @@ jobs: with: name: e2e-android-artifacts path: | - .maestro/screenshots/**/*_diff.png + .maestro/screenshots/android/.maestro/screenshots/android/*_diff.png From aae1a89a9abd6b525c346f3e1599d17fa5036fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Tue, 31 Mar 2026 08:14:28 +0200 Subject: [PATCH 22/22] fix: restore test files --- .github/workflows/e2e.yml | 4 +- .../flows/codeblock_no_link_detection.yaml | 24 ++++++ .maestro/flows/codeblock_style_blocking.yaml | 50 +++++++++++++ .../flows/conflicting_paragraph_merge.yaml | 59 +++++++++++++++ .maestro/flows/core_controls_smoke.yaml | 56 ++++++++++++++ .maestro/flows/empty_element_parsing.yaml | 34 +++++++++ .maestro/flows/initial_html_parsing.yaml | 22 ++++++ .../inline_styles_survive_block_toggle.yaml | 29 ++++++++ .maestro/flows/inline_styles_visual.yaml | 50 +++++++++++++ .maestro/flows/list_newline_insertion.yaml | 44 +++++++++++ ...sing_handles_single_quoted_attributes.yaml | 19 +++++ .maestro/flows/ordered_list_renumbering.yaml | 43 +++++++++++ .maestro/flows/paragraph_style_removal.yaml | 35 +++++++++ .maestro/flows/paragraph_style_toggle.yaml | 34 +++++++++ .../flows/paragraph_styles_blocks_visual.yaml | 34 +++++++++ .../paragraph_styles_headings_visual.yaml | 46 ++++++++++++ .maestro/flows/placeholder_visual.yaml | 11 +++ .maestro/flows/scrolling_after_typing.yaml | 63 ++++++++++++++++ .maestro/flows/scrolling_set_value.yaml | 64 ++++++++++++++++ .../scrolling_with_paragraph_styles.yaml | 74 +++++++++++++++++++ 20 files changed, 793 insertions(+), 2 deletions(-) create mode 100644 .maestro/flows/codeblock_no_link_detection.yaml create mode 100644 .maestro/flows/codeblock_style_blocking.yaml create mode 100644 .maestro/flows/conflicting_paragraph_merge.yaml create mode 100644 .maestro/flows/core_controls_smoke.yaml create mode 100644 .maestro/flows/empty_element_parsing.yaml create mode 100644 .maestro/flows/initial_html_parsing.yaml create mode 100644 .maestro/flows/inline_styles_survive_block_toggle.yaml create mode 100644 .maestro/flows/inline_styles_visual.yaml create mode 100644 .maestro/flows/list_newline_insertion.yaml create mode 100644 .maestro/flows/mention_parsing_handles_single_quoted_attributes.yaml create mode 100644 .maestro/flows/ordered_list_renumbering.yaml create mode 100644 .maestro/flows/paragraph_style_removal.yaml create mode 100644 .maestro/flows/paragraph_style_toggle.yaml create mode 100644 .maestro/flows/paragraph_styles_blocks_visual.yaml create mode 100644 .maestro/flows/paragraph_styles_headings_visual.yaml create mode 100644 .maestro/flows/placeholder_visual.yaml create mode 100644 .maestro/flows/scrolling_after_typing.yaml create mode 100644 .maestro/flows/scrolling_set_value.yaml create mode 100644 .maestro/flows/scrolling_with_paragraph_styles.yaml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fb8a2d11..8d2bbb99 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -125,7 +125,7 @@ jobs: with: name: e2e-ios-artifacts path: | - .maestro/screenshots/**/*_diff.png + .maestro/screenshots/ios/*_diff.png e2e-android: needs: [changes] @@ -188,4 +188,4 @@ jobs: with: name: e2e-android-artifacts path: | - .maestro/screenshots/android/.maestro/screenshots/android/*_diff.png + .maestro/screenshots/android/*_diff.png diff --git a/.maestro/flows/codeblock_no_link_detection.yaml b/.maestro/flows/codeblock_no_link_detection.yaml new file mode 100644 index 00000000..58591351 --- /dev/null +++ b/.maestro/flows/codeblock_no_link_detection.yaml @@ -0,0 +1,24 @@ +appId: swmansion.enriched.example +--- +# PR #257 - fix: handle style conflicts during automatic links detection +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- tapOn: + id: "toolbar-code-block" + +- inputText: "Hello example.com" + +- assertVisible: + id: "toolbar-link" + enabled: false + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "codeblock_no_link_detection" diff --git a/.maestro/flows/codeblock_style_blocking.yaml b/.maestro/flows/codeblock_style_blocking.yaml new file mode 100644 index 00000000..962b2f76 --- /dev/null +++ b/.maestro/flows/codeblock_style_blocking.yaml @@ -0,0 +1,50 @@ +appId: swmansion.enriched.example +--- +# Verifies that inside a code block, inline styles and rich features are +# blocked in the toolbar. Code blocks do not support inline formatting, +# automatic link detection, or mention insertion. +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- tapOn: + id: "toolbar-code-block" + +- inputText: "ABC example.com" + +- assertVisible: + id: "toolbar-bold" + enabled: false + +- assertVisible: + id: "toolbar-italic" + enabled: false + +- assertVisible: + id: "toolbar-underline" + enabled: false + +- assertVisible: + id: "toolbar-strikethrough" + enabled: false + +- assertVisible: + id: "toolbar-inline-code" + enabled: false + +- assertVisible: + id: "toolbar-link" + enabled: false + +- assertVisible: + id: "toolbar-mention" + enabled: false + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "codeblock_style_blocking" diff --git a/.maestro/flows/conflicting_paragraph_merge.yaml b/.maestro/flows/conflicting_paragraph_merge.yaml new file mode 100644 index 00000000..920e7388 --- /dev/null +++ b/.maestro/flows/conflicting_paragraph_merge.yaml @@ -0,0 +1,59 @@ +appId: swmansion.enriched.example +--- +# PR #306 - fix: handle conflicts between paragraphs +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- tapOn: + id: "toolbar-code-block" +- inputText: "Code text" +- pressKey: Enter + +- tapOn: + id: "toolbar-code-block" +- tapOn: + id: "toolbar-heading-1" +- inputText: "H1" + +# Position cursor at start of H1 line +- tapOn: + id: "editor-input" + point: "5%,75%" + +- pressKey: Backspace + +- runFlow: + when: + platform: "android" + commands: + - pressKey: Backspace +- inputText: " " + +- tapOn: + id: "editor-input" + point: "75%, 50%" + +- pressKey: Enter +- tapOn: + id: "toolbar-code-block" + +- tapOn: + id: "toolbar-inline-code" +- inputText: "Inline code" + +- tapOn: + id: "editor-input" + point: "5%, 75%" + +- pressKey: Backspace +- inputText: " " + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "conflicting_paragraph_merge" diff --git a/.maestro/flows/core_controls_smoke.yaml b/.maestro/flows/core_controls_smoke.yaml new file mode 100644 index 00000000..6e229ac6 --- /dev/null +++ b/.maestro/flows/core_controls_smoke.yaml @@ -0,0 +1,56 @@ +appId: swmansion.enriched.example +--- +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- assertVisible: + id: "editor-input" + +- assertVisible: + id: "focus-button" +- assertVisible: + id: "blur-button" +- assertVisible: + id: "set-value-button" + + +- assertVisible: + id: "toolbar-bold" +- assertVisible: + id: "toolbar-italic" +- assertVisible: + id: "toolbar-underline" +- assertVisible: + id: "toolbar-strikethrough" +- assertVisible: + id: "toolbar-inline-code" +- assertVisible: + id: "toolbar-heading-1" +- assertVisible: + id: "toolbar-heading-2" +- assertVisible: + id: "toolbar-heading-3" +- assertVisible: + id: "toolbar-heading-4" +- assertVisible: + id: "toolbar-heading-5" +- assertVisible: + id: "toolbar-heading-6" +- assertVisible: + id: "toolbar-quote" +- assertVisible: + id: "toolbar-code-block" +- assertVisible: + id: "toolbar-image" +- assertVisible: + id: "toolbar-link" +- assertVisible: + id: "toolbar-mention" +- assertVisible: + id: "toolbar-unordered-list" +- assertVisible: + id: "toolbar-ordered-list" +- assertVisible: + id: "toolbar-checkbox-list" diff --git a/.maestro/flows/empty_element_parsing.yaml b/.maestro/flows/empty_element_parsing.yaml new file mode 100644 index 00000000..4429bf8e --- /dev/null +++ b/.maestro/flows/empty_element_parsing.yaml @@ -0,0 +1,34 @@ +appId: swmansion.enriched.example +tags: + - android-only +--- +# PR #284 - fix: parsing empty elements +- launchApp +- tapOn: + id: "toggle-screen-button" + +- runFlow: + file: "../subflows/set_editor_value.yaml" + env: + VALUE: > + +

A

+

B

+

A

+

B

+
    +
  • A
  • +
  • B
  • +
  • C
  • +
  • +
+
+ + +- tapOn: + id: "size-max-button" + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "empty_element_parsing" diff --git a/.maestro/flows/initial_html_parsing.yaml b/.maestro/flows/initial_html_parsing.yaml new file mode 100644 index 00000000..868867d6 --- /dev/null +++ b/.maestro/flows/initial_html_parsing.yaml @@ -0,0 +1,22 @@ +appId: swmansion.enriched.example +--- +# PR #462 - fix(android): parsing initial HTML +# Verifies that setting an HTML defaultValue with headings renders correctly +# without duplicate or extra tags being inserted. +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- runFlow: + file: "../subflows/set_editor_value.yaml" + env: + VALUE: "

Hello

World

!!!

" + +- tapOn: + id: "size-max-button" + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "initial_html_parsing" \ No newline at end of file diff --git a/.maestro/flows/inline_styles_survive_block_toggle.yaml b/.maestro/flows/inline_styles_survive_block_toggle.yaml new file mode 100644 index 00000000..b2e5bfb9 --- /dev/null +++ b/.maestro/flows/inline_styles_survive_block_toggle.yaml @@ -0,0 +1,29 @@ +appId: swmansion.enriched.example +--- +# PR #370 - fix: reapplying spans +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- tapOn: + id: "toolbar-bold" +- inputText: "Bold" + +- tapOn: + id: "toolbar-bold" +- inputText: " regular" + +- tapOn: + id: "toolbar-quote" + +- tapOn: + id: "toolbar-quote" + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "inline_styles_survive_block_toggle" diff --git a/.maestro/flows/inline_styles_visual.yaml b/.maestro/flows/inline_styles_visual.yaml new file mode 100644 index 00000000..f8f2719a --- /dev/null +++ b/.maestro/flows/inline_styles_visual.yaml @@ -0,0 +1,50 @@ +appId: swmansion.enriched.example +--- +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- inputText: "Plain " + +- tapOn: + id: "toolbar-bold" +- inputText: "bold" +- tapOn: + id: "toolbar-bold" +- inputText: " " + +- tapOn: + id: "toolbar-italic" +- inputText: "italic" +- tapOn: + id: "toolbar-italic" +- inputText: " " + +- tapOn: + id: "toolbar-underline" +- inputText: "underline" +- tapOn: + id: "toolbar-underline" +- inputText: " " + +- tapOn: + id: "toolbar-strikethrough" +- inputText: "strike-through" +- tapOn: + id: "toolbar-strikethrough" +- inputText: " " + +- tapOn: + id: "toolbar-inline-code" +- inputText: "code" +- tapOn: + id: "toolbar-inline-code" + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "inline_styles" diff --git a/.maestro/flows/list_newline_insertion.yaml b/.maestro/flows/list_newline_insertion.yaml new file mode 100644 index 00000000..79e0dfd1 --- /dev/null +++ b/.maestro/flows/list_newline_insertion.yaml @@ -0,0 +1,44 @@ +appId: swmansion.enriched.example +--- +# PR #401 - fix(Android): newline insertion in lists +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- tapOn: + id: "toolbar-ordered-list" + +- runFlow: + when: + platform: android + commands: + - inputText: "XXXXXXXXXXXXXXXXXXXXXXXX" + - tapOn: + id: "editor-input" + point: "50%,50%" +# iOS places the cursor on word boundaries by default. Precisely positioning +# the cursor would require a press-and-drag gesture, which Maestro does not +# support. As a workaround we type two words, tap between them (snapping to +# the word boundary/space), then delete the space so we end up with a +# continuous word and the cursor in the middle. +- runFlow: + when: + platform: ios + commands: + - inputText: "XXXXXXXXXXX XXXXXXXXXXXXX" + - tapOn: + id: "editor-input" + point: "50%,50%" + - pressKey: Backspace + + +- pressKey: Enter + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "list_newline_insertion" diff --git a/.maestro/flows/mention_parsing_handles_single_quoted_attributes.yaml b/.maestro/flows/mention_parsing_handles_single_quoted_attributes.yaml new file mode 100644 index 00000000..83d4df74 --- /dev/null +++ b/.maestro/flows/mention_parsing_handles_single_quoted_attributes.yaml @@ -0,0 +1,19 @@ +appId: swmansion.enriched.example +--- +# "PR #405 - fix(ios): mention parsing" +# Verifies that mentions with single-quoted HTML attributes are parsed and styled correctly. + +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- runFlow: + file: "../subflows/set_editor_value.yaml" + env: + VALUE: "@John Doe" + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "mention_parsing_single_quoted_attributes" diff --git a/.maestro/flows/ordered_list_renumbering.yaml b/.maestro/flows/ordered_list_renumbering.yaml new file mode 100644 index 00000000..9865c269 --- /dev/null +++ b/.maestro/flows/ordered_list_renumbering.yaml @@ -0,0 +1,43 @@ +appId: swmansion.enriched.example +--- +# PR #359 - fix: android span watcher +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- tapOn: + id: "toolbar-ordered-list" + +- inputText: "A" +- pressKey: Enter +- inputText: "B" +- pressKey: Enter +- inputText: "C" + +# Tap on the second line to position cursor there +- tapOn: + id: "editor-input" + point: "50%,50%" + +- pressKey: backspace +- pressKey: backspace + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "ordered_list_renumbering_after_emptying_second_line" + +- tapOn: + id: "editor-input" + point: "50%,50%" + +- pressKey: backspace + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "ordered_list_renumbering_after_deleting_second_line" diff --git a/.maestro/flows/paragraph_style_removal.yaml b/.maestro/flows/paragraph_style_removal.yaml new file mode 100644 index 00000000..c749a005 --- /dev/null +++ b/.maestro/flows/paragraph_style_removal.yaml @@ -0,0 +1,35 @@ +appId: swmansion.enriched.example +--- +# PR #430 - fix(Android): removing paragraph styles +# Verifies that backspacing text from a line in a code block does not strip +# paragraph styles from the remaining lines. +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- tapOn: + id: "toolbar-quote" + +- inputText: "A" +- pressKey: Enter +- inputText: "B" +- pressKey: Enter +- inputText: "C" + +# Tap on the second line +- tapOn: + id: "editor-input" + point: "40%,50%" + +# Clear second line +- pressKey: Backspace +- pressKey: Backspace + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "paragraph_style_after_removal" diff --git a/.maestro/flows/paragraph_style_toggle.yaml b/.maestro/flows/paragraph_style_toggle.yaml new file mode 100644 index 00000000..4e200abe --- /dev/null +++ b/.maestro/flows/paragraph_style_toggle.yaml @@ -0,0 +1,34 @@ +appId: swmansion.enriched.example +--- +# PR #428 - fix(android): merging and splitting paragraph styles Verifies that +# toggling off a paragraph style on one line does not affect the style of +# adjacent lines +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- tapOn: + id: "toolbar-quote" + +- inputText: "1" +- pressKey: Enter +- inputText: "2" +- pressKey: Enter +- inputText: "3" + +# Tap on the second line +- tapOn: + id: "editor-input" + point: "40%,50%" + +- tapOn: + id: "toolbar-quote" + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "paragraph_style_toggle" diff --git a/.maestro/flows/paragraph_styles_blocks_visual.yaml b/.maestro/flows/paragraph_styles_blocks_visual.yaml new file mode 100644 index 00000000..1a437c12 --- /dev/null +++ b/.maestro/flows/paragraph_styles_blocks_visual.yaml @@ -0,0 +1,34 @@ +appId: swmansion.enriched.example +--- +# Visually validates rendering of block-level paragraph styles: +# blockquote and code block, each with multiple lines. +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- tapOn: + id: "toolbar-quote" +- inputText: "Quote 1" +- pressKey: Enter +- inputText: "Quote 2" +- pressKey: Enter +- tapOn: + id: "toolbar-quote" + +- inputText: "Normal paragraph" +- pressKey: Enter + +- tapOn: + id: "toolbar-code-block" +- inputText: "Code 1" +- pressKey: Enter +- inputText: "Code 2" + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "paragraph_styles_blocks" diff --git a/.maestro/flows/paragraph_styles_headings_visual.yaml b/.maestro/flows/paragraph_styles_headings_visual.yaml new file mode 100644 index 00000000..70c426fd --- /dev/null +++ b/.maestro/flows/paragraph_styles_headings_visual.yaml @@ -0,0 +1,46 @@ +appId: swmansion.enriched.example +--- +# Visually validates rendering of all heading levels (H1–H6). +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- tapOn: + id: "toolbar-heading-1" +- inputText: "H1" +- pressKey: Enter + +- tapOn: + id: "toolbar-heading-2" +- inputText: "H2" +- pressKey: Enter + +- tapOn: + id: "toolbar-heading-3" +- inputText: "H3" +- pressKey: Enter + +- tapOn: + id: "toolbar-heading-4" +- inputText: "H4" +- pressKey: Enter + +- tapOn: + id: "toolbar-heading-5" +- inputText: "H5" +- pressKey: Enter + +- tapOn: + id: "toolbar-heading-6" +- inputText: "H6" +- tapOn: + id: "size-max-button" + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "paragraph_styles_headings" diff --git a/.maestro/flows/placeholder_visual.yaml b/.maestro/flows/placeholder_visual.yaml new file mode 100644 index 00000000..4861f20f --- /dev/null +++ b/.maestro/flows/placeholder_visual.yaml @@ -0,0 +1,11 @@ +appId: swmansion.enriched.example +--- +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "initial_placeholder" diff --git a/.maestro/flows/scrolling_after_typing.yaml b/.maestro/flows/scrolling_after_typing.yaml new file mode 100644 index 00000000..c9fa0b25 --- /dev/null +++ b/.maestro/flows/scrolling_after_typing.yaml @@ -0,0 +1,63 @@ +appId: swmansion.enriched.example +# Tests basic scrollability +--- + +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- inputText: "Line one" +- pressKey: Enter +- inputText: "Line two" +- pressKey: Enter +- inputText: "Line three" +- pressKey: Enter +- inputText: "Line four" +- pressKey: Enter +- inputText: "Line five" +- pressKey: Enter +- inputText: "Line six" +- pressKey: Enter +- inputText: "Line seven" +- pressKey: Enter +- inputText: "Line eight" +- pressKey: Enter +- inputText: "Line nine" +- pressKey: Enter +- inputText: "Line ten" + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "scroll_after_typing_long_content_bottom" + +- tapOn: + id: "focus-button" + +- swipe: + from: + id: "editor-input" + direction: DOWN + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "scroll_after_typing_long_content_top" + +- tapOn: + id: "editor-input" + point: "0%,0%" + +- swipe: + from: + id: "editor-input" + direction: UP + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "scroll_after_typing_long_content_bottom" diff --git a/.maestro/flows/scrolling_set_value.yaml b/.maestro/flows/scrolling_set_value.yaml new file mode 100644 index 00000000..80d38083 --- /dev/null +++ b/.maestro/flows/scrolling_set_value.yaml @@ -0,0 +1,64 @@ +appId: swmansion.enriched.example +# Tests scrollability when content is set via setValue +--- +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- runFlow: + file: "../subflows/set_editor_value.yaml" + env: + VALUE: >- + +

Line one

+

Line two

+

Line three

+

Line four

+

Line five

+

Line six

+

Line seven

+

Line eight

+

Line nine

+

Line ten

+ + +# iOS starts at top - screenshot top, then swipe to bottom +- runFlow: + when: + platform: iOS + commands: + - runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "scrolling_set_value_top" + - tapOn: + id: "focus-button" + - swipe: + from: + id: "editor-input" + direction: UP + - runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "scrolling_set_value_bottom" + +# Android starts at bottom - screenshot bottom, then swipe to top +- runFlow: + when: + platform: Android + commands: + - runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "scrolling_set_value_bottom" + - tapOn: + id: "focus-button" + - swipe: + from: + id: "editor-input" + direction: DOWN + - runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "scrolling_set_value_top" diff --git a/.maestro/flows/scrolling_with_paragraph_styles.yaml b/.maestro/flows/scrolling_with_paragraph_styles.yaml new file mode 100644 index 00000000..96e987fe --- /dev/null +++ b/.maestro/flows/scrolling_with_paragraph_styles.yaml @@ -0,0 +1,74 @@ +appId: swmansion.enriched.example +# Tests scrolling with mixed block formatting (lists, quotes) +--- +- launchApp + +- tapOn: + id: "toggle-screen-button" + +- tapOn: + id: "editor-input" + +- tapOn: + id: "toolbar-unordered-list" +- inputText: "Bullet item one" +- pressKey: Enter +- inputText: "Bullet item two" +- pressKey: Enter +- inputText: "Bullet item three" +- pressKey: Enter +- pressKey: Enter + +- tapOn: + id: "toolbar-quote" +- inputText: "Quoted line one" +- pressKey: Enter +- inputText: "Quoted line two" +- pressKey: Enter +- inputText: "Quoted line three" +- pressKey: Enter +- tapOn: + id: "toolbar-quote" + +- tapOn: + id: "toolbar-ordered-list" +- inputText: "Numbered item one" +- pressKey: Enter +- inputText: "Numbered item two" +- pressKey: Enter +- inputText: "Numbered item three" +- pressKey: Enter +- pressKey: Enter + +- pressKey: Backspace + +- inputText: "Plain text line one" +- pressKey: Enter +- inputText: "Plain text final" + +- swipe: + from: + id: "editor-input" + direction: DOWN + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "scrolling_paragraph_styles_top" + +- tapOn: + id: "focus-button" + +- tapOn: + id: "editor-input" + point: "0%,0%" + +- swipe: + from: + id: "editor-input" + direction: UP + +- runFlow: + file: "../subflows/capture_or_assert_screenshot.yaml" + env: + SCREENSHOT_NAME: "scrolling_paragraph_styles_bottom"