ReviewOS

also looking at this

stacks/wildloop

feat: native trail maps and complete native mobile screens

#1017
Draft chris wants to merge codex/trail-experience into main
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.

.github/workflows/mobile-e2e.ymlmodified+14-20
Changes to .github/workflows/mobile-e2e.yml
@@ -9,6 +9,7 @@ on:
99 - 'tests/native/**'
1010 - 'scripts/run-native-trail-e2e.ts'
1111 - 'scripts/run-native-trail-android-e2e.ts'
12 - 'scripts/run-owned-android-e2e.ts'
1213 - 'app/**'
1314 - 'config/mobile.ts'
1415 # The app icon config/mobile.ts points at; a bad one fails the iOS build.
@@ -23,6 +24,7 @@ on:
2324 - 'tests/native/**'
2425 - 'scripts/run-native-trail-e2e.ts'
2526 - 'scripts/run-native-trail-android-e2e.ts'
27 - 'scripts/run-owned-android-e2e.ts'
2628 - 'app/**'
2729 - 'config/mobile.ts'
2830 # The app icon config/mobile.ts points at; a bad one fails the iOS build.
@@ -93,6 +95,11 @@ jobs:
9395 - uses: gradle/actions/setup-gradle@v4
9496 with:
9597 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"
96103 - name: Enable KVM
97104 run: |
98105 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:
109116 - name: Build Android E2E app
110117 run: bun scripts/run-mobile-e2e.ts android --build-only
111118 - 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
121120 - name: Upload Android diagnostics
122121 if: always()
123122 uses: actions/upload-artifact@v5
@@ -184,7 +183,6 @@ jobs:
184183 timeout-minutes: 35
185184 env:
186185 QA_PORT_BASE: '7320'
187 NATIVE_QA_ANDROID_DEVICE: emulator-5554
188186 steps:
189187 - uses: actions/checkout@v5
190188 - uses: oven-sh/setup-bun@v2
@@ -197,6 +195,11 @@ jobs:
197195 - uses: gradle/actions/setup-gradle@v4
198196 with:
199197 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"
200203 - name: Install Maestro
201204 run: |
202205 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:
213216 sudo udevadm control --reload-rules
214217 sudo udevadm trigger --name-match=kvm
215218 - 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
226220 - name: Upload Android native trail evidence
227221 if: always()
228222 uses: actions/upload-artifact@v5
scripts/run-mobile-e2e.tsmodified+10-9
Changes to scripts/run-mobile-e2e.ts
@@ -364,22 +364,23 @@ function appId(platform: MobilePlatform): string {
364364function runAndroid(preview: boolean): void {
365365 requireCommand('adb')
366366 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.')
368369
369370 const apk = process.env.MOBILE_E2E_APP
370371 ?? join(generatedRoot, 'android/app/build/outputs/apk/debug/app-debug.apk')
371372 requirePath(apk, 'Android E2E APK')
372 execute(['adb', '-s', devices[0], 'install', '-r', apk])
373 execute(['adb', '-s', device, 'install', '-r', apk])
373374 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}.`)
377378 }
378379 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)
383384 }
384385}
385386
scripts/run-owned-android-e2e.tsadded+58-0
Changes to scripts/run-owned-android-e2e.ts
@@ -0,0 +1,58 @@
1import { dirname, join, resolve } from 'node:path'
2import { createAndroidEmulator } from 'craft-native/android'
3
4/** Run the installed app on a disposable device owned by the published SDK. */
5export 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
58if (import.meta.main) await runOwnedAndroidE2e(process.argv[2])