also looking at this
feat: native trail maps and complete native mobile screens
#1017
60 files
+2505
-823
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -9,6 +9,7 @@ on: | ||
| 9 | 9 | - 'tests/native/**' |
| 10 | 10 | - 'scripts/run-native-trail-e2e.ts' |
| 11 | 11 | - 'scripts/run-native-trail-android-e2e.ts' |
| 12 | - 'scripts/run-owned-android-e2e.ts' | |
| 12 | 13 | - 'app/**' |
| 13 | 14 | - 'config/mobile.ts' |
| 14 | 15 | # The app icon config/mobile.ts points at; a bad one fails the iOS build. |
| @@ -23,6 +24,7 @@ on: | ||
| 23 | 24 | - 'tests/native/**' |
| 24 | 25 | - 'scripts/run-native-trail-e2e.ts' |
| 25 | 26 | - 'scripts/run-native-trail-android-e2e.ts' |
| 27 | - 'scripts/run-owned-android-e2e.ts' | |
| 26 | 28 | - 'app/**' |
| 27 | 29 | - 'config/mobile.ts' |
| 28 | 30 | # The app icon config/mobile.ts points at; a bad one fails the iOS build. |
| @@ -93,6 +95,11 @@ jobs: | ||
| 93 | 95 | - uses: gradle/actions/setup-gradle@v4 |
| 94 | 96 | with: |
| 95 | 97 | gradle-version: '8.11.1' |
| 98 | - uses: android-actions/setup-android@v4 | |
| 99 | with: | |
| 100 | packages: platform-tools | |
| 101 | - name: Install the published SDK compile platform | |
| 102 | run: sdkmanager "platforms;android-36" "build-tools;36.0.0" | |
| 96 | 103 | - name: Enable KVM |
| 97 | 104 | run: | |
| 98 | 105 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules |
| @@ -109,15 +116,7 @@ jobs: | ||
| 109 | 116 | - name: Build Android E2E app |
| 110 | 117 | run: bun scripts/run-mobile-e2e.ts android --build-only |
| 111 | 118 | - name: Run Android E2E flows |
| 112 | uses: reactivecircus/android-emulator-runner@v2 | |
| 113 | with: | |
| 114 | api-level: 34 | |
| 115 | target: google_apis | |
| 116 | arch: x86_64 | |
| 117 | profile: pixel_7 | |
| 118 | ram-size: 4096M | |
| 119 | disable-animations: true | |
| 120 | script: bun scripts/run-mobile-e2e.ts android --skip-build | |
| 119 | run: bun scripts/run-owned-android-e2e.ts baseline | |
| 121 | 120 | - name: Upload Android diagnostics |
| 122 | 121 | if: always() |
| 123 | 122 | uses: actions/upload-artifact@v5 |
| @@ -184,7 +183,6 @@ jobs: | ||
| 184 | 183 | timeout-minutes: 35 |
| 185 | 184 | env: |
| 186 | 185 | QA_PORT_BASE: '7320' |
| 187 | NATIVE_QA_ANDROID_DEVICE: emulator-5554 | |
| 188 | 186 | steps: |
| 189 | 187 | - uses: actions/checkout@v5 |
| 190 | 188 | - uses: oven-sh/setup-bun@v2 |
| @@ -197,6 +195,11 @@ jobs: | ||
| 197 | 195 | - uses: gradle/actions/setup-gradle@v4 |
| 198 | 196 | with: |
| 199 | 197 | gradle-version: '8.11.1' |
| 198 | - uses: android-actions/setup-android@v4 | |
| 199 | with: | |
| 200 | packages: platform-tools | |
| 201 | - name: Install the published SDK compile platform | |
| 202 | run: sdkmanager "platforms;android-36" "build-tools;36.0.0" | |
| 200 | 203 | - name: Install Maestro |
| 201 | 204 | run: | |
| 202 | 205 | curl -fsSL "https://github.com/mobile-dev-inc/Maestro/releases/download/${MAESTRO_VERSION}/maestro.zip" -o "$RUNNER_TEMP/maestro.zip" |
| @@ -213,16 +216,7 @@ jobs: | ||
| 213 | 216 | sudo udevadm control --reload-rules |
| 214 | 217 | sudo udevadm trigger --name-match=kvm |
| 215 | 218 | - name: Test native maps, sticky actions and persisted reviews |
| 216 | uses: reactivecircus/android-emulator-runner@v2 | |
| 217 | with: | |
| 218 | api-level: 34 | |
| 219 | target: default | |
| 220 | arch: x86_64 | |
| 221 | profile: pixel_7 | |
| 222 | ram-size: 4096M | |
| 223 | emulator-options: -no-window -no-snapshot -gpu swiftshader_indirect -noaudio -no-boot-anim | |
| 224 | disable-animations: false | |
| 225 | script: bun scripts/run-native-trail-android-e2e.ts --skip-build | |
| 219 | run: bun scripts/run-owned-android-e2e.ts native-trail | |
| 226 | 220 | - name: Upload Android native trail evidence |
| 227 | 221 | if: always() |
| 228 | 222 | uses: actions/upload-artifact@v5 |
| @@ -364,22 +364,23 @@ function appId(platform: MobilePlatform): string { | ||
| 364 | 364 | function runAndroid(preview: boolean): void { |
| 365 | 365 | requireCommand('adb') |
| 366 | 366 | const devices = parseAdbDevices(execute(['adb', 'devices'], { capture: true })) |
| 367 | if (devices.length !== 1) throw new Error(`Expected exactly one ready Android device, found ${devices.length}`) | |
| 367 | const device = process.env.ANDROID_SERIAL ?? (devices.length === 1 ? devices[0] : undefined) | |
| 368 | if (!device || !devices.includes(device)) throw new Error('Select a ready Android device with ANDROID_SERIAL, or connect exactly one device.') | |
| 368 | 369 | |
| 369 | 370 | const apk = process.env.MOBILE_E2E_APP |
| 370 | 371 | ?? join(generatedRoot, 'android/app/build/outputs/apk/debug/app-debug.apk') |
| 371 | 372 | requirePath(apk, 'Android E2E APK') |
| 372 | execute(['adb', '-s', devices[0], 'install', '-r', apk]) | |
| 373 | execute(['adb', '-s', device, 'install', '-r', apk]) | |
| 373 | 374 | if (preview) { |
| 374 | execute(['adb', '-s', devices[0], 'shell', 'am', 'force-stop', appId('android')]) | |
| 375 | execute(['adb', '-s', devices[0], 'shell', 'monkey', '-p', appId('android'), '-c', 'android.intent.category.LAUNCHER', '1']) | |
| 376 | console.log(`Wildloop is open on Android device ${devices[0]}.`) | |
| 375 | execute(['adb', '-s', device, 'shell', 'am', 'force-stop', appId('android')]) | |
| 376 | execute(['adb', '-s', device, 'shell', 'monkey', '-p', appId('android'), '-c', 'android.intent.category.LAUNCHER', '1']) | |
| 377 | console.log(`Wildloop is open on Android device ${device}.`) | |
| 377 | 378 | } |
| 378 | 379 | else { |
| 379 | checkAndroidPlatform(devices[0]) | |
| 380 | seedTestLocation('android', devices[0]) | |
| 381 | runMaestroJourneys('android', devices[0], null) | |
| 382 | checkAndroidPlatform(devices[0]) | |
| 380 | checkAndroidPlatform(device) | |
| 381 | seedTestLocation('android', device) | |
| 382 | runMaestroJourneys('android', device, null) | |
| 383 | checkAndroidPlatform(device) | |
| 383 | 384 | } |
| 384 | 385 | } |
| 385 | 386 | |
| @@ -0,0 +1,58 @@ | ||
| 1 | import { dirname, join, resolve } from 'node:path' | |
| 2 | import { createAndroidEmulator } from 'craft-native/android' | |
| 3 | ||
| 4 | /** Run the installed app on a disposable device owned by the published SDK. */ | |
| 5 | export async function runOwnedAndroidE2e(suite: string | undefined): Promise<void> { | |
| 6 | if (suite !== 'baseline' && suite !== 'native-trail') | |
| 7 | throw new Error('Select the baseline or native-trail Android journey.') | |
| 8 | const sdkRoot = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT | |
| 9 | const sdkmanager = Bun.which('sdkmanager') | |
| 10 | const avdmanager = Bun.which('avdmanager') | |
| 11 | if (!sdkRoot || !sdkmanager || !avdmanager) | |
| 12 | throw new Error('Install the normal Android SDK command-line tools before native QA.') | |
| 13 | const root = resolve(`storage/framework/runtime/e2e/${suite === 'native-trail' ? 'native-trail-android' : 'android'}/emulator`) | |
| 14 | const abi = process.arch === 'arm64' ? 'arm64-v8a' : 'x86_64' | |
| 15 | const session = await createAndroidEmulator({ | |
| 16 | sdkRoot, sdkmanager, avdmanager, | |
| 17 | image: `system-images;android-35;google_apis_playstore;${abi}`, | |
| 18 | acceleration: 'hardware', acceptLicenses: true, artifactsDirectory: root, | |
| 19 | }) | |
| 20 | try { | |
| 21 | const args = suite === 'native-trail' | |
| 22 | ? ['bun', 'scripts/run-native-trail-android-e2e.ts', '--skip-build'] | |
| 23 | : ['bun', 'scripts/run-mobile-e2e.ts', 'android', '--skip-build'] | |
| 24 | const test = Bun.spawn(args, { | |
| 25 | env: { | |
| 26 | ...session.env, PATH: `${dirname(session.adb)}:${session.env.PATH ?? ''}`, | |
| 27 | NATIVE_QA_ANDROID_DEVICE: session.serial, | |
| 28 | }, | |
| 29 | stdout: 'inherit', stderr: 'inherit', | |
| 30 | }) | |
| 31 | const status = await test.exited | |
| 32 | if (status !== 0) { | |
| 33 | for (const [name, args] of [ | |
| 34 | ['device-state', ['get-state']], | |
| 35 | ['platform-events', ['logcat', '-b', 'events', '-d', '-v', 'threadtime']], | |
| 36 | ['platform-properties', ['shell', 'getprop']], | |
| 37 | ] as const) { | |
| 38 | try { | |
| 39 | const diagnostic = Bun.spawn([session.adb, '-s', session.serial, ...args], { | |
| 40 | env: session.env, stdout: 'pipe', stderr: 'pipe', signal: AbortSignal.timeout(10_000), | |
| 41 | }) | |
| 42 | const [code, stdout, stderr] = await Promise.all([ | |
| 43 | diagnostic.exited, new Response(diagnostic.stdout).text(), new Response(diagnostic.stderr).text(), | |
| 44 | ]) | |
| 45 | await Bun.write(join(root, `failure-${name}.json`), JSON.stringify({ code, stdout, stderr }) + '\n') | |
| 46 | } | |
| 47 | catch (cause) { console.warn(`Could not retain Android ${name}: ${cause}`) } | |
| 48 | } | |
| 49 | throw new Error(`Android ${suite} acceptance failed with exit ${status}.`) | |
| 50 | } | |
| 51 | } | |
| 52 | finally { | |
| 53 | await session.dispose() | |
| 54 | await Bun.write(join(root, 'disposal.json'), JSON.stringify({ serial: session.serial, disposed: true }) + '\n') | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | if (import.meta.main) await runOwnedAndroidE2e(process.argv[2]) | |