Skip to content

Commit c9abb7b

Browse files
sunnylqmclaude
andcommitted
test(e2e): add Android debug-build boot smoke job
Release-only e2e can never hit code guarded by ReactBuildConfig.DEBUG, which is how the v10.48.2 startup crash ("Native Module Flow doesn't declare constants") slipped through. The new job builds the example app in debug, runs Metro on the runner with a prewarmed dev bundle, and asserts the app boots to the first screen — any debug-only native validation failure now fails CI. The smoke spec uses its own minimal jest config (no local update server or ppk artifacts) and is excluded from the main suite's testMatch. Locally: bun build:android-debug && bun smoke:android-debug. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent ab5c17e commit c9abb7b

5 files changed

Lines changed: 162 additions & 1 deletion

File tree

.github/workflows/e2e_android.yml

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,115 @@ jobs:
146146
if-no-files-found: ignore
147147
retention-days: 7
148148

149+
# Debug 构建启动冒烟:codegen 生成的 Java spec 把常量校验等检查包在
150+
# ReactBuildConfig.DEBUG 内,上面的 release e2e 物理上跑不到这些代码路径
151+
# (v10.48.2 的 "doesn't declare constants" 启动崩溃就是这样漏掉的)。
152+
# 只验证 debug 包能启动进首屏,不跑更新流,所以不需要更新 server 和产物。
153+
e2e-android-debug-smoke:
154+
runs-on: ubuntu-latest
155+
timeout-minutes: 30
156+
steps:
157+
- name: Checkout react-native-update
158+
uses: actions/checkout@v7
159+
with:
160+
submodules: recursive
161+
162+
- name: Setup Node.js
163+
uses: actions/setup-node@v6
164+
with:
165+
node-version: 24
166+
167+
- name: Setup Bun
168+
uses: oven-sh/setup-bun@v2
169+
with:
170+
bun-version: latest
171+
172+
- name: Cache Bun dependencies
173+
uses: actions/cache@v5
174+
with:
175+
path: ~/.bun/install/cache
176+
key: ${{ runner.os }}-bun-e2e-${{ hashFiles('Example/e2etest/bun.lock', 'react-native-update-cli/bun.lock') }}
177+
restore-keys: |
178+
${{ runner.os }}-bun-e2e-
179+
180+
- name: Setup Java
181+
uses: actions/setup-java@v5
182+
with:
183+
distribution: temurin
184+
java-version: '17'
185+
cache: gradle
186+
cache-dependency-path: |
187+
Example/e2etest/bun.lock
188+
Example/e2etest/android/*.gradle
189+
Example/e2etest/android/**/*.gradle
190+
Example/e2etest/android/gradle/wrapper/gradle-wrapper.properties
191+
192+
- name: Enable KVM
193+
run: |
194+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
195+
sudo udevadm control --reload-rules
196+
sudo udevadm trigger --name-match=kvm
197+
198+
- name: Install e2etest dependencies
199+
run: cd Example/e2etest && bun install --frozen-lockfile
200+
201+
- name: Link local react-native-update
202+
run: |
203+
set -euo pipefail
204+
export LOCAL_RNU_VERSION="$(node -p "require('./Example/e2etest/node_modules/react-native-update/package.json').version")"
205+
LOCAL_RNU_DIR="$PWD/Example/e2etest/.e2e-artifacts/local-react-native-update"
206+
rm -rf "$LOCAL_RNU_DIR" Example/e2etest/node_modules/react-native-update
207+
mkdir -p "$LOCAL_RNU_DIR"
208+
cp package.json react-native-update.podspec react-native.config.js expo-module.config.json "$LOCAL_RNU_DIR"/
209+
rsync -a --exclude='.git' src ios android cpp scripts "$LOCAL_RNU_DIR"/
210+
node -e "const fs = require('fs'); const file = 'Example/e2etest/.e2e-artifacts/local-react-native-update/package.json'; const pkg = JSON.parse(fs.readFileSync(file, 'utf8')); pkg.version = process.env.LOCAL_RNU_VERSION; fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');"
211+
ln -s ../.e2e-artifacts/local-react-native-update Example/e2etest/node_modules/react-native-update
212+
213+
- name: Detox build (android.emu.debug)
214+
env:
215+
DETOX_ANDROID_ARCHS: x86_64
216+
DETOX_AVD_NAME: api34
217+
run: cd Example/e2etest && bunx detox build --configuration android.emu.debug
218+
219+
- name: Start Metro in background
220+
run: |
221+
cd Example/e2etest
222+
mkdir -p .e2e-artifacts
223+
nohup bun start > .e2e-artifacts/metro.log 2>&1 &
224+
# 预热 dev bundle:趁模拟器启动的窗口把首个 bundle 编出来,
225+
# 避免 app 首次加载时现编,把冒烟等待时间全吃掉
226+
nohup bash -c 'for i in $(seq 1 90); do curl -sf http://localhost:8081/status >/dev/null && break; sleep 2; done; curl -sf --max-time 420 -o /dev/null "http://localhost:8081/index.bundle?platform=android&dev=true&minify=false"' > .e2e-artifacts/metro-prewarm.log 2>&1 &
227+
228+
- name: Detox test (android.emu.debug boot smoke)
229+
uses: reactivecircus/android-emulator-runner@v2
230+
env:
231+
DETOX_ANDROID_ARCHS: x86_64
232+
DETOX_AVD_NAME: api34
233+
with:
234+
api-level: 34
235+
target: google_apis
236+
arch: x86_64
237+
avd-name: api34
238+
emulator-boot-timeout: 900
239+
script: |
240+
cd Example/e2etest
241+
for i in $(seq 1 90); do curl -sf http://localhost:8081/status >/dev/null && break; sleep 2; done
242+
curl -sf --max-time 420 -o /dev/null "http://localhost:8081/index.bundle?platform=android&dev=true&minify=false" || { echo '::error::Metro dev bundle failed to build'; tail -n 100 .e2e-artifacts/metro.log; exit 1; }
243+
bunx detox test --configuration android.emu.debug --config e2e/smoke/jest.config.js --no-start --headless --record-logs all --retries 1
244+
245+
- name: Upload Detox artifacts
246+
# cancelled() 覆盖 job 超时被杀的场景,否则超时时拿不到现场
247+
if: failure() || cancelled()
248+
uses: actions/upload-artifact@v7.0.1
249+
with:
250+
name: e2e-android-debug-smoke-detox-artifacts
251+
path: |
252+
Example/e2etest/artifacts
253+
Example/e2etest/.e2e-artifacts/metro.log
254+
Example/e2etest/.e2e-artifacts/metro-prewarm.log
255+
if-no-files-found: ignore
256+
retention-days: 7
257+
149258
e2e-android-rn077-oldarch:
150259
runs-on: ubuntu-latest
151260
timeout-minutes: 35

Example/e2etest/e2e/jest.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const config = {
77
rootDir: '..',
88
testMatch: ['<rootDir>/e2e/**/*.test.ts'],
99
// Harmony tests use their own runner (harmony.jest.config.js), not Detox.
10-
testPathIgnorePatterns: ['/e2e/harmony/'],
10+
// The debug boot smoke has its own runner config too (smoke/jest.config.js).
11+
testPathIgnorePatterns: ['/e2e/harmony/', '/e2e/smoke/'],
1112
testTimeout: 300000,
1213
maxWorkers: 1,
1314
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { by, device, element, waitFor } from 'detox';
2+
3+
// Debug 构建启动冒烟:只验证 app 能进首屏。codegen 生成的 Java spec 里
4+
// 常量校验等检查包在 ReactBuildConfig.DEBUG 内,release e2e 全绿也拦不住
5+
// (如 v10.48.2 修的 "Native Module Flow doesn't declare constants")。
6+
// 首屏渲染前必须过 getConstants,崩了 bundle-label 永远不会出现。
7+
// CI 上 metro 冷启动首次出 dev bundle 可能要几分钟,超时给足。
8+
const BOOT_TIMEOUT = 300000;
9+
10+
describe('debug build boot smoke', () => {
11+
it('boots to the first screen', async () => {
12+
await device.launchApp({
13+
newInstance: true,
14+
...(device.getPlatform() === 'android'
15+
? { launchArgs: { detoxEnableSynchronization: '0' } }
16+
: {}),
17+
});
18+
19+
await waitFor(element(by.id('bundle-label')))
20+
.toBeVisible()
21+
.withTimeout(BOOT_TIMEOUT);
22+
});
23+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const path = require('node:path');
2+
3+
const moduleDir = __dirname;
4+
5+
// Debug 构建启动冒烟专用 runner 配置:不需要本地更新 server 和 ppk 产物,
6+
// 直接用 Detox 原生的 global hooks,跳过 ../globalSetup 的整套准备流程。
7+
/** @type {import('jest').Config} */
8+
const config = {
9+
rootDir: '../..',
10+
testMatch: ['<rootDir>/e2e/smoke/**/*.test.ts'],
11+
testTimeout: 420000,
12+
maxWorkers: 1,
13+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
14+
transform: {
15+
'^.+\\.(js|jsx|ts|tsx)$': [
16+
'babel-jest',
17+
{ configFile: path.resolve(moduleDir, '../../babel.config.js') },
18+
],
19+
},
20+
globalSetup: 'detox/runners/jest/globalSetup',
21+
globalTeardown: 'detox/runners/jest/globalTeardown',
22+
reporters: ['detox/runners/jest/reporter'],
23+
testEnvironment: 'detox/runners/jest/testEnvironment',
24+
verbose: true,
25+
};
26+
27+
module.exports = config;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"build:android-release": "cd Example/e2etest && bun && detox build --configuration android.emu.release",
2626
"test:android-release": "cd Example/e2etest && E2E_PLATFORM=android bun detox test --configuration android.emu.release --headless --record-logs all",
2727
"test:android-debug": "cd Example/e2etest && E2E_PLATFORM=android detox test --configuration android.emu.debug --headless --record-logs all",
28+
"smoke:android-debug": "cd Example/e2etest && detox test --configuration android.emu.debug --config e2e/smoke/jest.config.js",
2829
"e2e:ios": "bun build:ios-release && bun test:ios-release",
2930
"e2e:android": "bun build:android-release && bun test:android-release",
3031
"tests:emulator:prepare": "cd .github/workflows/scripts/functions && bun && bun build",

0 commit comments

Comments
 (0)