From 3618e3597da4dcf7377879fba5bbc5199167b77a Mon Sep 17 00:00:00 2001 From: floydkim Date: Fri, 20 Feb 2026 01:31:58 +0900 Subject: [PATCH 1/7] feat(e2e): add --only and failure summary to Expo matrix runner --- scripts/e2e/run-expo-matrix.sh | 80 +++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/scripts/e2e/run-expo-matrix.sh b/scripts/e2e/run-expo-matrix.sh index 29bb61e3..39b53e49 100755 --- a/scripts/e2e/run-expo-matrix.sh +++ b/scripts/e2e/run-expo-matrix.sh @@ -6,6 +6,10 @@ EXAMPLES_DIR="$ROOT_DIR/Examples" FORCE_RECREATE=0 SKIP_SETUP=0 +FAILED_E2E=() +PASSED_E2E=() +RUN_ANDROID=1 +RUN_IOS=1 while [[ $# -gt 0 ]]; do case "$1" in @@ -17,9 +21,30 @@ while [[ $# -gt 0 ]]; do SKIP_SETUP=1 shift ;; + --only) + if [[ $# -lt 2 ]]; then + echo "Missing value for --only (android|ios)" >&2 + exit 1 + fi + case "$2" in + android) + RUN_ANDROID=1 + RUN_IOS=0 + ;; + ios) + RUN_ANDROID=0 + RUN_IOS=1 + ;; + *) + echo "Invalid platform for --only: $2 (expected: android|ios)" >&2 + exit 1 + ;; + esac + shift 2 + ;; *) echo "Unknown option: $1" >&2 - echo "Usage: $0 [--force-recreate] [--skip-setup]" >&2 + echo "Usage: $0 [--force-recreate] [--skip-setup] [--only android|ios]" >&2 exit 1 ;; esac @@ -59,16 +84,43 @@ setup_app_if_needed() { fi } -run_e2e_for_app() { +run_e2e_for_app_platform() { local app_name="$1" + local platform="$2" + + if run_cmd npm run e2e -- --app "$app_name" --framework expo --platform "$platform"; then + PASSED_E2E+=("${app_name}:${platform}") + else + FAILED_E2E+=("${app_name}:${platform}") + echo "[warn] e2e failed (app=${app_name}, platform=${platform})" + fi +} - run_cmd npm run e2e -- --app "$app_name" --framework expo --platform android - run_cmd npm run e2e -- --app "$app_name" --framework expo --platform ios +print_e2e_summary() { + echo + echo "============================================================" + echo "[E2E SUMMARY]" + echo "============================================================" + echo "passed: ${#PASSED_E2E[@]}" + echo "failed: ${#FAILED_E2E[@]}" + + if [[ ${#FAILED_E2E[@]} -gt 0 ]]; then + echo + echo "Failed E2E targets:" + for failed in "${FAILED_E2E[@]}"; do + echo " - $failed" + done + fi } main() { cd "$ROOT_DIR" + if [[ "$RUN_ANDROID" -eq 0 && "$RUN_IOS" -eq 0 ]]; then + echo "Both platforms are skipped. Nothing to run." + return 0 + fi + local sdk54_app="Expo54" local sdk55_beta_app="Expo55Beta" @@ -77,14 +129,30 @@ main() { echo "[Expo] sdk=54 app=$sdk54_app" echo "============================================================" setup_app_if_needed "54" "false" "$sdk54_app" - run_e2e_for_app "$sdk54_app" + if [[ "$RUN_ANDROID" -eq 1 ]]; then + run_e2e_for_app_platform "$sdk54_app" "android" + fi + if [[ "$RUN_IOS" -eq 1 ]]; then + run_e2e_for_app_platform "$sdk54_app" "ios" + fi echo echo "============================================================" echo "[Expo] sdk=55 beta app=$sdk55_beta_app" echo "============================================================" setup_app_if_needed "55" "true" "$sdk55_beta_app" - run_e2e_for_app "$sdk55_beta_app" + if [[ "$RUN_ANDROID" -eq 1 ]]; then + run_e2e_for_app_platform "$sdk55_beta_app" "android" + fi + if [[ "$RUN_IOS" -eq 1 ]]; then + run_e2e_for_app_platform "$sdk55_beta_app" "ios" + fi + + print_e2e_summary + + if [[ ${#FAILED_E2E[@]} -gt 0 ]]; then + return 1 + fi } main From d94891de03f3b47d397e533ff6539f4fcfdf2cea Mon Sep 17 00:00:00 2001 From: floydkim Date: Fri, 20 Feb 2026 01:55:53 +0900 Subject: [PATCH 2/7] feat(e2e): add minor-based legacy architecture option for RN matrix --- scripts/e2e/run-rn-cli-matrix.sh | 33 +++++++++++++++++-- scripts/setupExampleApp/runSetupExampleApp.ts | 20 ++++++++--- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/scripts/e2e/run-rn-cli-matrix.sh b/scripts/e2e/run-rn-cli-matrix.sh index 0360137f..7d0324c8 100755 --- a/scripts/e2e/run-rn-cli-matrix.sh +++ b/scripts/e2e/run-rn-cli-matrix.sh @@ -24,6 +24,7 @@ FAILED_E2E=() PASSED_E2E=() RUN_ANDROID=1 RUN_IOS=1 +LEGACY_ARCH_MAX_MINOR=76 while [[ $# -gt 0 ]]; do case "$1" in @@ -56,6 +57,18 @@ while [[ $# -gt 0 ]]; do esac shift 2 ;; + --legacy-arch-max-version) + if [[ $# -lt 2 ]]; then + echo "Missing value for --legacy-arch-max-version (e.g. 76 for 0.76.x)" >&2 + exit 1 + fi + if ! [[ "$2" =~ ^[0-9]{2}$ ]]; then + echo "Invalid value for --legacy-arch-max-version: $2 (expected exactly two digits, e.g. 76 or 81)" >&2 + exit 1 + fi + LEGACY_ARCH_MAX_MINOR=$((10#$2)) + shift 2 + ;; --skip-platform) echo "Unknown option: $1" >&2 echo "Use --only android|ios instead." >&2 @@ -63,7 +76,7 @@ while [[ $# -gt 0 ]]; do ;; *) echo "Unknown option: $1" >&2 - echo "Usage: $0 [--force-recreate] [--skip-setup] [--only android|ios]" >&2 + echo "Usage: $0 [--force-recreate] [--skip-setup] [--only android|ios] [--legacy-arch-max-version ]" >&2 exit 1 ;; esac @@ -81,6 +94,17 @@ app_name_from_rn_version() { echo "RN${compact}" } +should_use_legacy_architecture() { + local rn_version="$1" + local rn_minor + if ! [[ "$rn_version" =~ ^[0-9]+\.([0-9]+)\.[0-9]+$ ]]; then + echo "Invalid RN version format: $rn_version (expected: ..)" >&2 + exit 1 + fi + rn_minor=$((10#${BASH_REMATCH[1]})) + [[ "$rn_minor" -le "$LEGACY_ARCH_MAX_MINOR" ]] +} + setup_app_if_needed() { local rn_version="$1" local app_name="$2" @@ -101,7 +125,12 @@ setup_app_if_needed() { fi fi - run_cmd npm run setup-example-app -- -v "$rn_version" + local setup_args=(npm run setup-example-app -- -v "$rn_version") + if should_use_legacy_architecture "$rn_version"; then + setup_args+=(--disable-new-architecture) + fi + + run_cmd "${setup_args[@]}" } run_e2e_for_app_platform() { diff --git a/scripts/setupExampleApp/runSetupExampleApp.ts b/scripts/setupExampleApp/runSetupExampleApp.ts index 92e967c9..c5ec11b1 100644 --- a/scripts/setupExampleApp/runSetupExampleApp.ts +++ b/scripts/setupExampleApp/runSetupExampleApp.ts @@ -9,6 +9,7 @@ interface SetupCliOptions { rnVersion: string; workingDir: string; skipPodInstall?: boolean; + disableNewArchitecture?: boolean; } interface SetupContext { @@ -17,6 +18,7 @@ interface SetupContext { workingDirectory: string; projectPath: string; skipPodInstall: boolean; + disableNewArchitecture: boolean; } type SetupStep = { @@ -41,6 +43,11 @@ const program = new Command() "--skip-pod-install", "Skip bundle install and bundle exec pod install during template postinstall", false + ) + .option( + "--disable-new-architecture", + "Disable new architecture (legacy architecture setup)", + false ); const setupSteps: SetupStep[] = [ @@ -61,7 +68,7 @@ const setupSteps: SetupStep[] = [ }, { name: "configure-new-architecture", - description: "Disable new architecture for RN <= 0.76", + description: "Configure new architecture (legacy for RN <= 0.76 by default)", run: configureNewArchitecture }, { @@ -115,7 +122,10 @@ async function main() { projectName, workingDirectory: workingDir, projectPath, - skipPodInstall: options.skipPodInstall ?? false + skipPodInstall: options.skipPodInstall ?? false, + disableNewArchitecture: + (options.disableNewArchitecture ?? false) || + shouldDisableNewArchitectureByDefault(normalizedVersion) }; await runSetupExampleApp(context); @@ -297,8 +307,8 @@ async function configureAndroidVersioning(context: SetupContext): Promise } async function configureNewArchitecture(context: SetupContext): Promise { - if (!shouldDisableNewArchitecture(context.rnVersion)) { - console.log("[skip] RN >= 0.77, keeping template new architecture setting"); + if (!context.disableNewArchitecture) { + console.log("[skip] keeping template new architecture setting"); return; } @@ -335,7 +345,7 @@ async function configureNewArchitecture(context: SetupContext): Promise { }); } -function shouldDisableNewArchitecture(rnVersion: string): boolean { +function shouldDisableNewArchitectureByDefault(rnVersion: string): boolean { return semver.lt(rnVersion, "0.77.0"); } From 371ace69a285c45e1f60ce2e5225859e89eaa75f Mon Sep 17 00:00:00 2001 From: floydkim Date: Fri, 20 Feb 2026 01:55:56 +0900 Subject: [PATCH 3/7] docs(e2e): add EN/KO guides for matrix runner scripts --- scripts/e2e/README.ko.md | 116 +++++++++++++++++++++++++++++++++++++++ scripts/e2e/README.md | 116 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 scripts/e2e/README.ko.md create mode 100644 scripts/e2e/README.md diff --git a/scripts/e2e/README.ko.md b/scripts/e2e/README.ko.md new file mode 100644 index 00000000..ccc5ebb1 --- /dev/null +++ b/scripts/e2e/README.ko.md @@ -0,0 +1,116 @@ +# E2E 매트릭스 실행 스크립트 + +이 디렉토리에는 여러 앱 변형을 대상으로 E2E 테스트를 실행하는 래퍼 스크립트가 있습니다. + +## 스크립트 + +- `scripts/e2e/run-rn-cli-matrix.sh` +- `scripts/e2e/run-expo-matrix.sh` + +--- + +## 1) `run-rn-cli-matrix.sh` + +React Native CLI 예제 앱(`RN0747` ~ `RN0840`)을 버전 매트릭스로 E2E 실행합니다. + +### 동작 + +1. `npm run setup-example-app`으로 앱 생성 (`--skip-setup`이면 생략) +2. 설정된 RN 버전 각각에 대해 E2E 실행 +3. 일부 타겟이 실패해도 다음 타겟 계속 실행 +4. 마지막에 성공/실패 개수와 실패 타겟 목록 출력 + +### 사용법 + +```bash +bash scripts/e2e/run-rn-cli-matrix.sh [옵션] +``` + +### 옵션 + +| 옵션 | 설명 | 기본값 | +|---|---|---| +| `--force-recreate` | 앱 디렉토리가 있어도 삭제 후 재생성 | `false` | +| `--skip-setup` | 앱 생성 단계를 건너뛰고 E2E만 실행 | `false` | +| `--only android\|ios` | 한 플랫폼만 실행 | 둘 다 | +| `--legacy-arch-max-version ` | RN **minor**가 이 값 이하인 버전을 legacy architecture로 셋업 | `76` | + +### `--legacy-arch-max-version` 입력 형식 + +- 정확히 **두 자리 숫자**만 지원합니다. +- `76` 입력 시 `0.76.x` 이하를 legacy architecture로 셋업합니다. +- `81` 입력 시 `0.81.x` 이하를 legacy architecture로 셋업합니다. +- patch 버전은 의도적으로 무시합니다. + +예시: + +```bash +# 기본 임계값(76): 0.76.x 이하 legacy +bash scripts/e2e/run-rn-cli-matrix.sh + +# android만 실행 +bash scripts/e2e/run-rn-cli-matrix.sh --only android + +# 0.81.x 이하를 legacy로 셋업 +bash scripts/e2e/run-rn-cli-matrix.sh --legacy-arch-max-version 81 + +# setup 생략 + iOS만 실행 +bash scripts/e2e/run-rn-cli-matrix.sh --skip-setup --only ios +``` + +### 종료 코드 + +- `0`: 모든 타겟 성공 +- `1`: 하나 이상 실패 + +--- + +## 2) `run-expo-matrix.sh` + +Expo 예제 앱(`Expo54`, `Expo55Beta`)을 매트릭스로 E2E 실행합니다. + +### 동작 + +1. `npm run setup-expo-example-app`으로 앱 생성 (`--skip-setup`이면 생략) +2. Expo 앱/플랫폼별로 E2E 실행 +3. 일부 타겟이 실패해도 다음 타겟 계속 실행 +4. 마지막에 성공/실패 개수와 실패 타겟 목록 출력 + +### 사용법 + +```bash +bash scripts/e2e/run-expo-matrix.sh [옵션] +``` + +### 옵션 + +| 옵션 | 설명 | 기본값 | +|---|---|---| +| `--force-recreate` | 앱 디렉토리가 있어도 삭제 후 재생성 | `false` | +| `--skip-setup` | 앱 생성 단계를 건너뛰고 E2E만 실행 | `false` | +| `--only android\|ios` | 한 플랫폼만 실행 | 둘 다 | + +예시: + +```bash +# 전체 Expo 매트릭스 (setup + android + ios) +bash scripts/e2e/run-expo-matrix.sh + +# android만 실행 +bash scripts/e2e/run-expo-matrix.sh --only android + +# 앱 재생성 후 iOS만 실행 +bash scripts/e2e/run-expo-matrix.sh --force-recreate --only ios +``` + +### 종료 코드 + +- `0`: 모든 타겟 성공 +- `1`: 하나 이상 실패 + +--- + +## 참고 + +- 경로 해석 문제를 줄이기 위해 저장소 루트에서 실행하는 것을 권장합니다. +- 두 스크립트 모두 타겟 단위 실패 시 즉시 중단하지 않고, 마지막에 전체 실패 목록을 출력하도록 설계되어 있습니다. diff --git a/scripts/e2e/README.md b/scripts/e2e/README.md new file mode 100644 index 00000000..97849488 --- /dev/null +++ b/scripts/e2e/README.md @@ -0,0 +1,116 @@ +# E2E Matrix Runner Scripts + +This directory contains wrapper scripts to run E2E tests across multiple app variants. + +## Scripts + +- `scripts/e2e/run-rn-cli-matrix.sh` +- `scripts/e2e/run-expo-matrix.sh` + +--- + +## 1) `run-rn-cli-matrix.sh` + +Runs E2E tests for React Native CLI example apps (`RN0747` ~ `RN0840`) as a version matrix. + +### What it does + +1. Creates apps with `npm run setup-example-app` (unless `--skip-setup` is used). +2. Runs E2E for each configured RN version. +3. Continues even if some targets fail. +4. Prints a final summary with passed/failed counts and failed targets. + +### Usage + +```bash +bash scripts/e2e/run-rn-cli-matrix.sh [options] +``` + +### Options + +| Option | Description | Default | +|---|---|---| +| `--force-recreate` | Recreate app directories even if they already exist | `false` | +| `--skip-setup` | Skip app setup and run E2E only | `false` | +| `--only android\|ios` | Run only one platform | both | +| `--legacy-arch-max-version ` | Use legacy architecture setup for RN versions whose **minor** is less than or equal to this value | `76` | + +### `--legacy-arch-max-version` format + +- Exactly 2 digits only. +- `76` means `0.76.x` and below use legacy architecture setup. +- `81` means `0.81.x` and below use legacy architecture setup. +- Patch version is ignored by design. + +Examples: + +```bash +# Default threshold (76): legacy for 0.76.x and below +bash scripts/e2e/run-rn-cli-matrix.sh + +# Run android only +bash scripts/e2e/run-rn-cli-matrix.sh --only android + +# Use legacy setup up to 0.81.x +bash scripts/e2e/run-rn-cli-matrix.sh --legacy-arch-max-version 81 + +# Skip setup and run iOS only +bash scripts/e2e/run-rn-cli-matrix.sh --skip-setup --only ios +``` + +### Exit code + +- `0`: all targets passed +- `1`: one or more targets failed + +--- + +## 2) `run-expo-matrix.sh` + +Runs E2E tests for Expo example apps (`Expo54`, `Expo55Beta`) as a matrix. + +### What it does + +1. Creates Expo apps with `npm run setup-expo-example-app` (unless `--skip-setup` is used). +2. Runs E2E for each Expo app and platform. +3. Continues even if some targets fail. +4. Prints a final summary with passed/failed counts and failed targets. + +### Usage + +```bash +bash scripts/e2e/run-expo-matrix.sh [options] +``` + +### Options + +| Option | Description | Default | +|---|---|---| +| `--force-recreate` | Recreate app directories even if they already exist | `false` | +| `--skip-setup` | Skip app setup and run E2E only | `false` | +| `--only android\|ios` | Run only one platform | both | + +Examples: + +```bash +# Full Expo matrix (setup + android + ios) +bash scripts/e2e/run-expo-matrix.sh + +# Android only +bash scripts/e2e/run-expo-matrix.sh --only android + +# Recreate apps and run iOS only +bash scripts/e2e/run-expo-matrix.sh --force-recreate --only ios +``` + +### Exit code + +- `0`: all targets passed +- `1`: one or more targets failed + +--- + +## Notes + +- Run from repository root for predictable paths. +- Both scripts intentionally continue after per-target failures and report all failures at the end. From 9cb2c86e29eb797ec94f5018a548bdd51d6b89aa Mon Sep 17 00:00:00 2001 From: floydkim Date: Sat, 21 Feb 2026 00:44:58 +0900 Subject: [PATCH 4/7] feat(e2e): migrate to maestro-runner with iOS team id support --- .github/workflows/e2e-manual.yml | 8 +- .gitignore | 6 ++ e2e/README.ko.md | 15 +-- e2e/README.md | 15 +-- e2e/helpers/resolve-ios-team-id.test.js | 89 ++++++++++++++++ e2e/helpers/resolve-ios-team-id.ts | 135 ++++++++++++++++++++++++ e2e/run.ts | 78 +++++++++++--- 7 files changed, 312 insertions(+), 34 deletions(-) create mode 100644 e2e/helpers/resolve-ios-team-id.test.js create mode 100644 e2e/helpers/resolve-ios-team-id.ts diff --git a/.github/workflows/e2e-manual.yml b/.github/workflows/e2e-manual.yml index ca89bf1e..1ca2794e 100644 --- a/.github/workflows/e2e-manual.yml +++ b/.github/workflows/e2e-manual.yml @@ -64,9 +64,9 @@ jobs: restore-keys: | ${{ runner.os }}-pods-${{ inputs.app }}- - - name: Install Maestro CLI + - name: Install maestro-runner run: | - curl -Ls "https://get.maestro.mobile.dev" | bash + curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash echo "$HOME/.maestro/bin" >> "$GITHUB_PATH" - name: Select iOS simulator @@ -125,9 +125,9 @@ jobs: run: npm i working-directory: Examples/${{ inputs.app }} - - name: Install Maestro CLI + - name: Install maestro-runner run: | - curl -Ls "https://get.maestro.mobile.dev" | bash + curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash echo "$HOME/.maestro/bin" >> "$GITHUB_PATH" - name: Run Android E2E diff --git a/.gitignore b/.gitignore index c4fc49f8..bbdb5c99 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,9 @@ bin/* # E2E mock server data e2e/mock-server/data/ + +# maestro-runner report directory +e2e/reports/ + +# maestro-runner iOS driver artifacts +drivers diff --git a/e2e/README.ko.md b/e2e/README.ko.md index 29ead7a6..1a51649a 100644 --- a/e2e/README.ko.md +++ b/e2e/README.ko.md @@ -1,11 +1,11 @@ # E2E 테스트 실행 가이드 -[Maestro](https://maestro.mobile.dev/)를 사용한 `react-native-code-push` E2E 테스트입니다. +[maestro-runner](https://github.com/devicelab-dev/maestro-runner)를 사용한 `react-native-code-push` E2E 테스트입니다. ## 사전 요구사항 - **Node.js** (v18 이상) -- **Maestro CLI** — [설치 가이드](https://maestro.mobile.dev/getting-started/installing-maestro) +- **maestro-runner** — `curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash` - **iOS**: Xcode 및 부팅된 iOS 시뮬레이터 - **Android**: Android SDK 및 실행 중인 에뮬레이터 - `Examples/` 디렉토리에 설정된 예제 앱 (예: `RN0840`) @@ -16,7 +16,7 @@ # 전체 실행 (빌드 + 테스트) npm run e2e -- --app RN0840 --platform ios -# 빌드 생략, Maestro 플로우만 실행 +# 빌드 생략, 테스트 플로우만 실행 npm run e2e -- --app RN0840 --platform ios --maestro-only ``` @@ -26,7 +26,7 @@ npm run e2e -- --app RN0840 --platform ios --maestro-only # Expo 예제 앱 전체 실행 npm run e2e -- --app Expo55 --framework expo --platform ios -# Expo 예제 앱 Maestro만 실행 +# Expo 예제 앱 플로우만 실행 npm run e2e -- --app Expo55Beta --framework expo --platform ios --maestro-only ``` @@ -38,7 +38,8 @@ npm run e2e -- --app Expo55Beta --framework expo --platform ios --maestro-only | `--platform ` | 예 | `ios` 또는 `android` | | `--framework ` | 아니오 | Expo 예제 앱인 경우 `expo` 지정 | | `--simulator ` | 아니오 | iOS 시뮬레이터 이름 (부팅된 시뮬레이터 자동 감지, 기본값 "iPhone 16") | -| `--maestro-only` | 아니오 | 빌드 단계 생략, Maestro 플로우만 실행 | +| `--maestro-only` | 아니오 | 빌드 단계 생략, 테스트 플로우만 실행 | +| `--team-id ` | 아니오 | iOS WDA 서명용 Apple Team ID (`maestro-runner`). iOS에서 생략하면 env/keychain/profile에서 자동 탐지 | ## 실행 과정 @@ -50,7 +51,7 @@ npm run e2e -- --app Expo55Beta --framework expo --platform ios --maestro-only 2. **앱 빌드** — 예제 앱을 Release 모드로 빌드하여 시뮬레이터/에뮬레이터에 설치합니다. 3. **번들 준비** — `npx code-push release`로 릴리스 히스토리를 생성하고 v1.0.1을 번들링합니다. 4. **Mock 서버 시작** — 번들과 릴리스 히스토리 JSON을 서빙하는 로컬 HTTP 서버(포트 18081)를 시작합니다. -5. **Maestro 플로우 실행**: +5. **테스트 플로우 실행 (maestro-runner 사용)**: - `01-app-launch` — 앱 실행 및 UI 요소 존재 확인 - `02-restart-no-crash` — 재시작 탭 후 크래시 없음 확인 - `03-update-flow` — 이전 업데이트 초기화, sync 트리거, 업데이트 설치 확인("UPDATED!" 표시) 및 메타데이터 `METADATA_V1.0.1` 확인 @@ -102,6 +103,6 @@ e2e/ ## 문제 해결 - **iOS 빌드 시 서명 오류**: setup 스크립트가 `SUPPORTED_PLATFORMS = iphonesimulator`를 설정하고 코드 서명을 비활성화합니다. `scripts/setupExampleApp`으로 예제 앱이 설정되었는지 확인하세요. -- **Maestro가 앱을 찾지 못함**: 실행 전에 시뮬레이터/에뮬레이터가 부팅되어 있는지 확인하세요. iOS의 경우 스크립트가 부팅된 시뮬레이터를 자동 감지합니다. +- **maestro-runner가 앱을 찾지 못함**: 실행 전에 시뮬레이터/에뮬레이터가 부팅되어 있는지 확인하세요. iOS의 경우 스크립트가 부팅된 시뮬레이터를 자동 감지합니다. - **Android 네트워크 오류**: Android 에뮬레이터는 호스트 머신의 localhost에 접근하기 위해 `10.0.2.2`를 사용합니다. 설정에서 자동으로 처리됩니다. - **업데이트가 적용되지 않음**: Mock 서버가 실행 중인지(포트 18081), `mock-server/data/`에 예상되는 번들과 히스토리 파일이 있는지 확인하세요. diff --git a/e2e/README.md b/e2e/README.md index 7fa83004..a29801e8 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -1,11 +1,11 @@ # E2E Testing Guide -End-to-end tests for `react-native-code-push` using [Maestro](https://maestro.mobile.dev/). +End-to-end tests for `react-native-code-push` using [maestro-runner](https://github.com/devicelab-dev/maestro-runner). ## Prerequisites - **Node.js** (v18+) -- **Maestro CLI** — [Install guide](https://maestro.mobile.dev/getting-started/installing-maestro) +- **maestro-runner** — `curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash` - **iOS**: Xcode with a booted iOS Simulator - **Android**: Android SDK with a running emulator - An example app set up under `Examples/` (e.g. `RN0840`) @@ -16,7 +16,7 @@ End-to-end tests for `react-native-code-push` using [Maestro](https://maestro.mo # Full run (build + test) npm run e2e -- --app RN0840 --platform ios -# Skip build, run Maestro flows only +# Skip build, run test flows only npm run e2e -- --app RN0840 --platform ios --maestro-only ``` @@ -26,7 +26,7 @@ npm run e2e -- --app RN0840 --platform ios --maestro-only # Full run for Expo example app npm run e2e -- --app Expo55 --framework expo --platform ios -# Maestro-only run for Expo example app +# Flow-only run for Expo example app npm run e2e -- --app Expo55Beta --framework expo --platform ios --maestro-only ``` @@ -38,7 +38,8 @@ npm run e2e -- --app Expo55Beta --framework expo --platform ios --maestro-only | `--platform ` | Yes | `ios` or `android` | | `--framework ` | No | Use `expo` for Expo example apps | | `--simulator ` | No | iOS simulator name (auto-detects booted simulator, defaults to "iPhone 16") | -| `--maestro-only` | No | Skip build step, only run Maestro flows | +| `--maestro-only` | No | Skip build step, only run test flows | +| `--team-id ` | No | Apple Team ID for iOS WDA signing (`maestro-runner`). If omitted on iOS, the runner auto-detects from env/keychain/profiles | ## What It Does @@ -50,7 +51,7 @@ The test runner (`e2e/run.ts`) executes these phases in order: 2. **Build app** — Builds the example app in Release mode and installs it on the simulator/emulator. 3. **Prepare bundle** — Creates release history and bundles v1.0.1 using `npx code-push release`. 4. **Start mock server** — Starts a local HTTP server (port 18081) that serves bundles and release history JSON. -5. **Run Maestro flows** — Executes: +5. **Run test flows (via maestro-runner)** — Executes: - `01-app-launch` — Verifies the app launches and UI elements are present. - `02-restart-no-crash` — Taps Restart, confirms app doesn't crash. - `03-update-flow` — Clears any previous update, triggers sync, verifies update installs (shows "UPDATED!") and metadata shows `METADATA_V1.0.1`. @@ -102,6 +103,6 @@ When creating multiple releases with identical source code (e.g. v1.0.1 and v1.0 ## Troubleshooting - **Build fails with signing error (iOS)**: The setup script sets `SUPPORTED_PLATFORMS = iphonesimulator` and disables code signing. Make sure the example app was set up with `scripts/setupExampleApp`. -- **Maestro can't find the app**: Ensure the simulator/emulator is booted before running. For iOS, the script auto-detects the booted simulator. +- **maestro-runner can't find the app**: Ensure the simulator/emulator is booted before running. For iOS, the script auto-detects the booted simulator. - **Android network error**: Android emulators use `10.0.2.2` to reach the host machine's localhost. This is handled automatically by the config. - **Update not applying**: Check that the mock server is running (port 18081) and that `mock-server/data/` contains the expected bundle and history files. diff --git a/e2e/helpers/resolve-ios-team-id.test.js b/e2e/helpers/resolve-ios-team-id.test.js new file mode 100644 index 00000000..e010a2bf --- /dev/null +++ b/e2e/helpers/resolve-ios-team-id.test.js @@ -0,0 +1,89 @@ +const { resolveIosTeamIdForMaestro } = require("./resolve-ios-team-id"); + +function createProvider({ certificateOutput = "", profileOutput = "" } = {}) { + return { + readCertificateSubjectOutput: () => certificateOutput, + readProfileTeamIdOutput: () => profileOutput, + }; +} + +describe("resolveIosTeamIdForMaestro", () => { + test("returns undefined for android", () => { + const result = resolveIosTeamIdForMaestro({ + platform: "android", + provider: createProvider({ + certificateOutput: "subject= /OU=AAAAAAAAAA/CN=Apple Development: Test", + }), + }); + + expect(result).toBeUndefined(); + }); + + test("uses CLI team id when provided", () => { + const result = resolveIosTeamIdForMaestro({ + platform: "ios", + cliTeamId: "ABCDEFGHIJ", + provider: createProvider({ + certificateOutput: "subject= /OU=ZZZZZZZZZZ/CN=Apple Development: Test", + }), + }); + + expect(result).toBe("ABCDEFGHIJ"); + }); + + test("uses environment team id when CLI team id is missing", () => { + const result = resolveIosTeamIdForMaestro({ + platform: "ios", + env: { APPLE_TEAM_ID: "BCDEFGHIJK" }, + provider: createProvider(), + }); + + expect(result).toBe("BCDEFGHIJK"); + }); + + test("uses detected team id from certificates when one value exists", () => { + const result = resolveIosTeamIdForMaestro({ + platform: "ios", + provider: createProvider({ + certificateOutput: [ + "subject= /UID=ABCDEF/OU=KLMNOPQRST/CN=Apple Development: Tester", + "subject= /UID=ABCDEF/OU=KLMNOPQRST/CN=Apple Development: Tester 2", + ].join("\n"), + }), + }); + + expect(result).toBe("KLMNOPQRST"); + }); + + test("uses detected team id from profiles when certificate output is empty", () => { + const result = resolveIosTeamIdForMaestro({ + platform: "ios", + provider: createProvider({ + profileOutput: ["ZYXWVUTSRQ", "ZYXWVUTSRQ"].join("\n"), + }), + }); + + expect(result).toBe("ZYXWVUTSRQ"); + }); + + test("throws when multiple team ids are detected", () => { + expect(() => + resolveIosTeamIdForMaestro({ + platform: "ios", + provider: createProvider({ + certificateOutput: "subject= /OU=AAAAAAAAAA/CN=Apple Development: Test", + profileOutput: "BBBBBBBBBB", + }), + }), + ).toThrow("Multiple iOS Team IDs detected"); + }); + + test("throws when no team id is available", () => { + expect(() => + resolveIosTeamIdForMaestro({ + platform: "ios", + provider: createProvider(), + }), + ).toThrow("Could not resolve iOS Team ID"); + }); +}); diff --git a/e2e/helpers/resolve-ios-team-id.ts b/e2e/helpers/resolve-ios-team-id.ts new file mode 100644 index 00000000..ee07c09c --- /dev/null +++ b/e2e/helpers/resolve-ios-team-id.ts @@ -0,0 +1,135 @@ +import { execSync } from "child_process"; + +type Platform = "ios" | "android"; + +interface TeamIdProvider { + readCertificateSubjectOutput: () => string; + readProfileTeamIdOutput: () => string; +} + +interface ResolveIosTeamIdOptions { + platform: Platform; + cliTeamId?: string; + env?: NodeJS.ProcessEnv; + provider?: TeamIdProvider; +} + +const TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/; +const TEAM_ID_ENV_KEYS = [ + "MAESTRO_IOS_TEAM_ID", + "APPLE_TEAM_ID", + "IOS_TEAM_ID", + "TEAM_ID", +] as const; + +export function resolveIosTeamIdForMaestro( + options: ResolveIosTeamIdOptions, +): string | undefined { + if (options.platform !== "ios") { + return undefined; + } + + const cliTeamId = normalizeTeamId(options.cliTeamId); + if (cliTeamId !== undefined) { + assertValidTeamId(cliTeamId, "--team-id"); + return cliTeamId; + } + + const env = options.env ?? process.env; + for (const key of TEAM_ID_ENV_KEYS) { + const value = normalizeTeamId(env[key]); + if (value === undefined) { + continue; + } + assertValidTeamId(value, key); + return value; + } + + const provider = options.provider ?? createDefaultProvider(); + const detectedTeamIds = uniqueTeamIds([ + ...extractTeamIds(provider.readCertificateSubjectOutput()), + ...extractTeamIds(provider.readProfileTeamIdOutput()), + ]); + + if (detectedTeamIds.length === 1) { + return detectedTeamIds[0]; + } + + if (detectedTeamIds.length > 1) { + throw new Error( + `Multiple iOS Team IDs detected: ${detectedTeamIds.join(", ")}. ` + + "Pass --team-id or set MAESTRO_IOS_TEAM_ID.", + ); + } + + throw new Error( + "Could not resolve iOS Team ID for maestro-runner. " + + "Pass --team-id or set MAESTRO_IOS_TEAM_ID.", + ); +} + +function createDefaultProvider(): TeamIdProvider { + return { + readCertificateSubjectOutput: () => + runCommand("security find-identity -v -p codesigning 2>/dev/null"), + readProfileTeamIdOutput: () => + runCommand( + "for p in \"$HOME\"/Library/MobileDevice/Provisioning\\ Profiles/*.mobileprovision; do " + + "security cms -D -i \"$p\" 2>/dev/null | " + + "plutil -extract TeamIdentifier.0 raw -o - - 2>/dev/null; " + + "done", + ), + }; +} + +function runCommand(command: string): string { + try { + return execSync(command, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); + } catch { + return ""; + } +} + +function normalizeTeamId(rawValue: string | undefined): string | undefined { + if (typeof rawValue !== "string") { + return undefined; + } + + const normalized = rawValue.trim().toUpperCase(); + return normalized.length > 0 ? normalized : undefined; +} + +function assertValidTeamId(teamId: string, source: string): void { + if (!TEAM_ID_PATTERN.test(teamId)) { + throw new Error( + `Invalid iOS Team ID from ${source}: "${teamId}". ` + + "Expected a 10-character uppercase alphanumeric value.", + ); + } +} + +function extractTeamIds(output: string): string[] { + const teamIds = new Set(); + + addMatches(teamIds, output, /OU=([A-Z0-9]{10})/g); + addMatches(teamIds, output, /\(([A-Z0-9]{10})\)/g); + + for (const rawLine of output.split(/\r?\n/)) { + const line = normalizeTeamId(rawLine); + if (line !== undefined && TEAM_ID_PATTERN.test(line)) { + teamIds.add(line); + } + } + + return Array.from(teamIds); +} + +function addMatches(target: Set, output: string, pattern: RegExp): void { + for (const match of output.matchAll(pattern)) { + target.add(match[1]); + } +} + +function uniqueTeamIds(teamIds: string[]): string[] { + return Array.from(new Set(teamIds)).sort(); +} diff --git a/e2e/run.ts b/e2e/run.ts index ce39c8f4..11544b79 100644 --- a/e2e/run.ts +++ b/e2e/run.ts @@ -7,6 +7,7 @@ import { prepareConfig, restoreConfig } from "./helpers/prepare-config"; import { prepareBundle, runCodePushCommand, setReleasingBundle, setReleaseMarker, clearReleaseMarker, getCodePushReleaseArgs } from "./helpers/prepare-bundle"; import { buildApp } from "./helpers/build-app"; import { startMockServer, stopMockServer } from "./mock-server/server"; +import { resolveIosTeamIdForMaestro } from "./helpers/resolve-ios-team-id"; interface CliOptions { app: string; @@ -14,16 +15,18 @@ interface CliOptions { framework?: "expo"; simulator?: string; maestroOnly?: boolean; + teamId?: string; } const program = new Command() .name("e2e") - .description("Run E2E tests with Maestro for CodePush example apps") + .description("Run E2E tests with maestro-runner for CodePush example apps") .requiredOption("--app ", "Example app name (e.g. RN0840RC5)") .requiredOption("--platform ", "Platform: ios or android") .option("--framework ", "Framework: expo") .option("--simulator ", "iOS simulator name (default: booted)") - .option("--maestro-only", "Skip build, only run Maestro flows", false); + .option("--maestro-only", "Skip build, only run test flows", false) + .option("--team-id ", "Apple Team ID for iOS WDA signing"); async function main() { const options = program.parse(process.argv).opts(); @@ -40,8 +43,17 @@ async function main() { await syncLocalLibraryIfAvailable(appPath, options.maestroOnly ?? false); const releaseIdentifier = getCodePushReleaseIdentifier(appPath); + let iosTeamId: string | undefined; try { + iosTeamId = resolveIosTeamIdForMaestro({ + platform: options.platform, + cliTeamId: options.teamId, + }); + if (options.platform === "ios" && iosTeamId) { + console.log(`[ios] using team id: ${iosTeamId}`); + } + // 1. Prepare config console.log("\n=== [prepare] ==="); prepareConfig(appPath, options.platform); @@ -67,7 +79,7 @@ async function main() { // 5. Run Maestro — Phase 1: main flows console.log("\n=== [run-maestro: phase 1] ==="); const flowsDir = path.resolve(__dirname, "flows"); - await runMaestro(flowsDir, options.platform, appId); + await runMaestro(flowsDir, options.platform, appId, iosTeamId); // 6. Disable release for rollback test console.log("\n=== [disable-release] ==="); @@ -84,7 +96,7 @@ async function main() { // 7. Run Maestro — Phase 2: rollback to binary console.log("\n=== [run-maestro: phase 2 (rollback to binary)] ==="); const rollbackDir = path.resolve(__dirname, "flows-rollback"); - await runMaestro(rollbackDir, options.platform, appId); + await runMaestro(rollbackDir, options.platform, appId, iosTeamId); // 8. Prepare partial rollback: release 1.0.1 + 1.0.2 with different hashes console.log("\n=== [prepare-bundle: partial rollback] ==="); @@ -125,7 +137,7 @@ async function main() { // 9. Run Maestro — update to 1.0.2 console.log("\n=== [run-maestro: partial rollback — update to 1.0.2] ==="); const updateFlow = path.resolve(__dirname, "flows-partial-rollback/01-update-to-latest.yaml"); - await runMaestro(updateFlow, options.platform, appId); + await runMaestro(updateFlow, options.platform, appId, iosTeamId); // 10. Disable only 1.0.2 → rollback target is 1.0.1 (not binary) console.log("\n=== [disable-release: 1.0.2 only] ==="); @@ -140,7 +152,7 @@ async function main() { // 11. Run Maestro — rollback from 1.0.2 to 1.0.1 console.log("\n=== [run-maestro: partial rollback — rollback to 1.0.1] ==="); const rollbackFlow = path.resolve(__dirname, "flows-partial-rollback/02-rollback-to-previous.yaml"); - await runMaestro(rollbackFlow, options.platform, appId); + await runMaestro(rollbackFlow, options.platform, appId, iosTeamId); console.log("\n=== E2E tests passed ==="); } catch (error) { @@ -152,6 +164,7 @@ async function main() { console.log("\n=== [cleanup] ==="); await stopMockServer(); restoreConfig(appPath); + await stopGradleDaemonIfNeeded(appPath, options.platform); } } @@ -231,23 +244,32 @@ function buildCodePushBundleIdentifier(appName: string): string { return `com.${normalized}`; } -function runMaestro(flowsDir: string, platform: "ios" | "android", appId: string): Promise { - const args = ["test", flowsDir, "--env", `APP_ID=${appId}`]; - - if (platform === "android") { - args.push("--platform", "android"); - } else { - args.push("--platform", "ios"); +function runMaestro( + flowsDir: string, + platform: "ios" | "android", + appId: string, + teamId?: string, +): Promise { + // Root directory for maestro-runner report outputs. + const reportRootDir = path.resolve(__dirname, "reports"); + fs.mkdirSync(reportRootDir, { recursive: true }); + const args = ["--platform", platform]; + if (platform === "ios") { + if (!teamId) { + throw new Error("iOS Team ID is required for maestro-runner. Pass --team-id ."); + } + args.push("--team-id", teamId); } + args.push("test", "--output", reportRootDir, "--env", `APP_ID=${appId}`, flowsDir); - console.log(`[command] maestro ${args.join(" ")}`); + console.log(`[command] maestro-runner ${args.join(" ")}`); return new Promise((resolve, reject) => { - const child = spawn("maestro", args, { stdio: "inherit" }); + const child = spawn("maestro-runner", args, { stdio: "inherit" }); child.on("error", reject); child.on("close", (code) => { if (code === 0) resolve(); - else reject(new Error(`Maestro tests failed (exit code: ${code})`)); + else reject(new Error(`maestro-runner tests failed (exit code: ${code})`)); }); }); } @@ -321,6 +343,30 @@ function resetAppStateBeforeFlows( }); } +// Android builds can leave Gradle daemon (java) processes running; stop them best-effort between E2E runs. +function stopGradleDaemonIfNeeded( + appPath: string, + platform: "ios" | "android", +): Promise { + if (platform !== "android") { + return Promise.resolve(); + } + + const androidPath = path.join(appPath, "android"); + console.log(`[command] ./gradlew --stop (cwd: ${androidPath})`); + + return new Promise((resolve) => { + const child = spawn("./gradlew", ["--stop"], { cwd: androidPath, stdio: "inherit" }); + child.once("error", (error) => { + console.warn(`[warn] gradle daemon stop failed: ${error.message}`); + resolve(); + }); + child.once("close", () => { + resolve(); + }); + }); +} + function syncLocalLibraryIfAvailable(appPath: string, maestroOnly: boolean): Promise { const packageJsonPath = path.join(appPath, "package.json"); if (!fs.existsSync(packageJsonPath)) { From 1ce2b5196e487d934a4b8523f7246caba4ac80e7 Mon Sep 17 00:00:00 2001 From: floydkim Date: Sat, 21 Feb 2026 00:45:09 +0900 Subject: [PATCH 5/7] chore(e2e): extend rn-cli matrix runner options --- scripts/e2e/run-rn-cli-matrix.sh | 42 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/scripts/e2e/run-rn-cli-matrix.sh b/scripts/e2e/run-rn-cli-matrix.sh index 7d0324c8..4c67f39a 100755 --- a/scripts/e2e/run-rn-cli-matrix.sh +++ b/scripts/e2e/run-rn-cli-matrix.sh @@ -6,16 +6,6 @@ EXAMPLES_DIR="$ROOT_DIR/Examples" RN_VERSIONS=( "0.74.7" - "0.75.5" - "0.76.9" - "0.77.3" - "0.78.3" - "0.79.7" - "0.80.3" - "0.81.6" - "0.82.1" - "0.83.2" - "0.84.0" ) FORCE_RECREATE=0 @@ -25,7 +15,15 @@ PASSED_E2E=() RUN_ANDROID=1 RUN_IOS=1 LEGACY_ARCH_MAX_MINOR=76 - +MAESTRO_ONLY=0 +ONLY_SETUP=0 + +# CLI options: +# --force-recreate: remove and recreate existing Examples/RNxxxx app directories +# --skip-setup: skip app setup and run with the current workspace state +# --maestro-only: skip build and run Maestro flows only +# --only-setup: run setup only and skip E2E execution +# --only android|ios: run E2E for the selected platform only while [[ $# -gt 0 ]]; do case "$1" in --force-recreate) @@ -36,6 +34,14 @@ while [[ $# -gt 0 ]]; do SKIP_SETUP=1 shift ;; + --maestro-only) + MAESTRO_ONLY=1 + shift + ;; + --only-setup) + ONLY_SETUP=1 + shift + ;; --only) if [[ $# -lt 2 ]]; then echo "Missing value for --only (android|ios)" >&2 @@ -76,7 +82,7 @@ while [[ $# -gt 0 ]]; do ;; *) echo "Unknown option: $1" >&2 - echo "Usage: $0 [--force-recreate] [--skip-setup] [--only android|ios] [--legacy-arch-max-version ]" >&2 + echo "Usage: $0 [--force-recreate] [--skip-setup] [--maestro-only] [--only-setup] [--only android|ios] [--legacy-arch-max-version ]" >&2 exit 1 ;; esac @@ -136,8 +142,13 @@ setup_app_if_needed() { run_e2e_for_app_platform() { local app_name="$1" local platform="$2" + local e2e_args=(--app "$app_name" --platform "$platform") - if run_cmd npm run e2e -- --app "$app_name" --platform "$platform"; then + if [[ "$MAESTRO_ONLY" -eq 1 ]]; then + e2e_args+=(--maestro-only) + fi + + if run_cmd npm run e2e -- "${e2e_args[@]}"; then PASSED_E2E+=("${app_name}:${platform}") else FAILED_E2E+=("${app_name}:${platform}") @@ -175,6 +186,11 @@ main() { setup_app_if_needed "$rn_version" "$local_app_name" done + if [[ "$ONLY_SETUP" -eq 1 ]]; then + echo "[done] setup completed (--only-setup)" + return 0 + fi + if [[ "$RUN_ANDROID" -eq 1 ]]; then echo echo "############################################################" From fdec5efcd36393c183e187a741c49b44bfa06abb Mon Sep 17 00:00:00 2001 From: floydkim Date: Sat, 21 Feb 2026 01:08:36 +0900 Subject: [PATCH 6/7] chore(e2e): restore rn-cli matrix versions and sync docs --- scripts/e2e/README.ko.md | 18 +++++++++++++++--- scripts/e2e/README.md | 18 +++++++++++++++--- scripts/e2e/run-rn-cli-matrix.sh | 10 ++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/scripts/e2e/README.ko.md b/scripts/e2e/README.ko.md index ccc5ebb1..16d2d86a 100644 --- a/scripts/e2e/README.ko.md +++ b/scripts/e2e/README.ko.md @@ -11,15 +11,19 @@ ## 1) `run-rn-cli-matrix.sh` -React Native CLI 예제 앱(`RN0747` ~ `RN0840`)을 버전 매트릭스로 E2E 실행합니다. +React Native CLI 예제 앱을 버전 매트릭스로 E2E 실행합니다. ### 동작 1. `npm run setup-example-app`으로 앱 생성 (`--skip-setup`이면 생략) -2. 설정된 RN 버전 각각에 대해 E2E 실행 +2. 스크립트의 `RN_VERSIONS`에 설정된 RN 버전 각각에 대해 E2E 실행 3. 일부 타겟이 실패해도 다음 타겟 계속 실행 4. 마지막에 성공/실패 개수와 실패 타겟 목록 출력 +현재 스크립트 기준 매트릭스 대상: + +- `0.74.7` (`RN0747`)부터 `0.84.0` (`RN0840`)까지 + ### 사용법 ```bash @@ -32,6 +36,8 @@ bash scripts/e2e/run-rn-cli-matrix.sh [옵션] |---|---|---| | `--force-recreate` | 앱 디렉토리가 있어도 삭제 후 재생성 | `false` | | `--skip-setup` | 앱 생성 단계를 건너뛰고 E2E만 실행 | `false` | +| `--maestro-only` | 빌드를 건너뛰고 Maestro 플로우만 실행 | `false` | +| `--only-setup` | setup만 수행하고 E2E 실행은 생략 | `false` | | `--only android\|ios` | 한 플랫폼만 실행 | 둘 다 | | `--legacy-arch-max-version ` | RN **minor**가 이 값 이하인 버전을 legacy architecture로 셋업 | `76` | @@ -51,6 +57,12 @@ bash scripts/e2e/run-rn-cli-matrix.sh # android만 실행 bash scripts/e2e/run-rn-cli-matrix.sh --only android +# setup만 실행 +bash scripts/e2e/run-rn-cli-matrix.sh --only-setup + +# Maestro 플로우만 실행 +bash scripts/e2e/run-rn-cli-matrix.sh --maestro-only + # 0.81.x 이하를 legacy로 셋업 bash scripts/e2e/run-rn-cli-matrix.sh --legacy-arch-max-version 81 @@ -72,7 +84,7 @@ Expo 예제 앱(`Expo54`, `Expo55Beta`)을 매트릭스로 E2E 실행합니다. ### 동작 1. `npm run setup-expo-example-app`으로 앱 생성 (`--skip-setup`이면 생략) -2. Expo 앱/플랫폼별로 E2E 실행 +2. Expo 앱/플랫폼별로 E2E 실행 (`npm run e2e -- --framework expo ...`) 3. 일부 타겟이 실패해도 다음 타겟 계속 실행 4. 마지막에 성공/실패 개수와 실패 타겟 목록 출력 diff --git a/scripts/e2e/README.md b/scripts/e2e/README.md index 97849488..8bf8e805 100644 --- a/scripts/e2e/README.md +++ b/scripts/e2e/README.md @@ -11,15 +11,19 @@ This directory contains wrapper scripts to run E2E tests across multiple app var ## 1) `run-rn-cli-matrix.sh` -Runs E2E tests for React Native CLI example apps (`RN0747` ~ `RN0840`) as a version matrix. +Runs E2E tests for React Native CLI example apps as a version matrix. ### What it does 1. Creates apps with `npm run setup-example-app` (unless `--skip-setup` is used). -2. Runs E2E for each configured RN version. +2. Runs E2E for each configured RN version (`RN_VERSIONS` in script). 3. Continues even if some targets fail. 4. Prints a final summary with passed/failed counts and failed targets. +Current matrix targets in script: + +- `0.74.7` (`RN0747`) through `0.84.0` (`RN0840`) + ### Usage ```bash @@ -32,6 +36,8 @@ bash scripts/e2e/run-rn-cli-matrix.sh [options] |---|---|---| | `--force-recreate` | Recreate app directories even if they already exist | `false` | | `--skip-setup` | Skip app setup and run E2E only | `false` | +| `--maestro-only` | Skip build and run Maestro flows only | `false` | +| `--only-setup` | Run setup only and skip E2E execution | `false` | | `--only android\|ios` | Run only one platform | both | | `--legacy-arch-max-version ` | Use legacy architecture setup for RN versions whose **minor** is less than or equal to this value | `76` | @@ -51,6 +57,12 @@ bash scripts/e2e/run-rn-cli-matrix.sh # Run android only bash scripts/e2e/run-rn-cli-matrix.sh --only android +# Setup only +bash scripts/e2e/run-rn-cli-matrix.sh --only-setup + +# Run Maestro flows only +bash scripts/e2e/run-rn-cli-matrix.sh --maestro-only + # Use legacy setup up to 0.81.x bash scripts/e2e/run-rn-cli-matrix.sh --legacy-arch-max-version 81 @@ -72,7 +84,7 @@ Runs E2E tests for Expo example apps (`Expo54`, `Expo55Beta`) as a matrix. ### What it does 1. Creates Expo apps with `npm run setup-expo-example-app` (unless `--skip-setup` is used). -2. Runs E2E for each Expo app and platform. +2. Runs E2E for each Expo app and platform (`npm run e2e -- --framework expo ...`). 3. Continues even if some targets fail. 4. Prints a final summary with passed/failed counts and failed targets. diff --git a/scripts/e2e/run-rn-cli-matrix.sh b/scripts/e2e/run-rn-cli-matrix.sh index 4c67f39a..71ab94b9 100755 --- a/scripts/e2e/run-rn-cli-matrix.sh +++ b/scripts/e2e/run-rn-cli-matrix.sh @@ -6,6 +6,16 @@ EXAMPLES_DIR="$ROOT_DIR/Examples" RN_VERSIONS=( "0.74.7" + "0.75.5" + "0.76.9" + "0.77.3" + "0.78.3" + "0.79.7" + "0.80.3" + "0.81.6" + "0.82.1" + "0.83.2" + "0.84.0" ) FORCE_RECREATE=0 From 6fa9a62fc3648a7bf9bd1c9a8222084c4c83b9db Mon Sep 17 00:00:00 2001 From: floydkim Date: Sat, 21 Feb 2026 01:23:53 +0900 Subject: [PATCH 7/7] chore(e2e): remove --skip-platform branch and document legacy-arch option --- scripts/e2e/run-rn-cli-matrix.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/e2e/run-rn-cli-matrix.sh b/scripts/e2e/run-rn-cli-matrix.sh index 71ab94b9..f4180e4c 100755 --- a/scripts/e2e/run-rn-cli-matrix.sh +++ b/scripts/e2e/run-rn-cli-matrix.sh @@ -34,6 +34,7 @@ ONLY_SETUP=0 # --maestro-only: skip build and run Maestro flows only # --only-setup: run setup only and skip E2E execution # --only android|ios: run E2E for the selected platform only +# --legacy-arch-max-version : use legacy architecture setup for RN x.y.z when y <= given minor while [[ $# -gt 0 ]]; do case "$1" in --force-recreate) @@ -85,11 +86,6 @@ while [[ $# -gt 0 ]]; do LEGACY_ARCH_MAX_MINOR=$((10#$2)) shift 2 ;; - --skip-platform) - echo "Unknown option: $1" >&2 - echo "Use --only android|ios instead." >&2 - exit 1 - ;; *) echo "Unknown option: $1" >&2 echo "Usage: $0 [--force-recreate] [--skip-setup] [--maestro-only] [--only-setup] [--only android|ios] [--legacy-arch-max-version ]" >&2