Skip to content

Commit 4989803

Browse files
committed
chore: upgrade E2E environment to Node 22, Detox 20.50.1, and Android API 34 with improved test stability
1 parent f5c9e4c commit 4989803

6 files changed

Lines changed: 264 additions & 33 deletions

File tree

.github/workflows/e2e_android.yml

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ concurrency:
1616
jobs:
1717
e2e-android:
1818
runs-on: macos-latest
19-
timeout-minutes: 10
19+
timeout-minutes: 30
2020
steps:
2121
- name: Checkout react-native-update
2222
uses: actions/checkout@v4
@@ -30,7 +30,7 @@ jobs:
3030
- name: Setup Node.js
3131
uses: actions/setup-node@v6
3232
with:
33-
node-version: 20
33+
node-version: 22
3434

3535
- name: Setup Bun
3636
uses: oven-sh/setup-bun@v2
@@ -43,25 +43,24 @@ jobs:
4343
distribution: temurin
4444
java-version: '17'
4545

46-
- name: Install testHotUpdate dependencies
47-
run: cd Example/testHotUpdate && bun install --frozen-lockfile
46+
- name: Install e2etest dependencies
47+
run: cd Example/e2etest && bun install --frozen-lockfile
4848

4949
- name: Install react-native-update-cli dependencies
5050
run: cd react-native-update-cli && bun install --frozen-lockfile
5151

5252
- name: Detox build (android.emu.release)
53-
run: cd Example/testHotUpdate && E2E_PLATFORM=android npx detox build --configuration android.emu.release
54-
55-
- name: Read Detox emulator name
56-
id: device
57-
run: node -e "console.log('AVD_NAME=' + require('./Example/testHotUpdate/.detoxrc').devices.emulator.device.avdName)" >> $GITHUB_OUTPUT
53+
env:
54+
DETOX_AVD_NAME: api34
55+
run: cd Example/e2etest && E2E_PLATFORM=android bunx detox build --configuration android.emu.release
5856

5957
- name: Detox test (android.emu.release)
6058
uses: reactivecircus/android-emulator-runner@v2
6159
env:
6260
RNU_CLI_ROOT: ${{ github.workspace }}/react-native-update-cli
61+
DETOX_AVD_NAME: api34
6362
with:
64-
api-level: 33
63+
api-level: 34
6564
arch: arm64-v8a
66-
avd-name: ${{ steps.device.outputs.AVD_NAME }}
67-
script: cd Example/testHotUpdate && E2E_PLATFORM=android npx detox test --configuration android.emu.release --headless --record-logs all
65+
avd-name: api34
66+
script: cd Example/e2etest && E2E_PLATFORM=android bunx detox test --configuration android.emu.release --headless --record-logs all

.github/workflows/e2e_ios.yml

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ concurrency:
1515
jobs:
1616
e2e-ios:
1717
runs-on: macos-latest
18-
timeout-minutes: 10
18+
timeout-minutes: 30
1919
steps:
2020
- name: Checkout react-native-update
2121
uses: actions/checkout@v4
@@ -29,7 +29,7 @@ jobs:
2929
- name: Setup Node.js
3030
uses: actions/setup-node@v6
3131
with:
32-
node-version: 20
32+
node-version: 22
3333

3434
- name: Setup Bun
3535
uses: oven-sh/setup-bun@v2
@@ -50,24 +50,27 @@ jobs:
5050
- name: Install applesimutils
5151
run: HOMEBREW_NO_AUTO_UPDATE=1 brew tap wix/brew && HOMEBREW_NO_AUTO_UPDATE=1 brew install applesimutils
5252

53-
- name: Install testHotUpdate dependencies
54-
run: cd Example/testHotUpdate && bun install --frozen-lockfile
53+
- name: Install e2etest dependencies
54+
run: cd Example/e2etest && bun install --frozen-lockfile
5555

5656
- name: Install react-native-update-cli dependencies
5757
run: cd react-native-update-cli && bun install --frozen-lockfile
5858

59+
- name: Install CocoaPods dependencies
60+
run: cd Example/e2etest/ios && pod install
61+
5962
- name: Rebuild Detox iOS framework cache
6063
run: |
61-
cd Example/testHotUpdate
62-
npx detox clean-framework-cache
63-
npx detox build-framework-cache
64+
cd Example/e2etest
65+
bunx detox clean-framework-cache
66+
bunx detox build-framework-cache
6467
6568
- name: Detox build (ios.sim.release)
6669
env:
6770
RNU_CLI_ROOT: ${{ github.workspace }}/react-native-update-cli
68-
run: cd Example/testHotUpdate && E2E_PLATFORM=ios npx detox build --configuration ios.sim.release
71+
run: cd Example/e2etest && E2E_PLATFORM=ios bunx detox build --configuration ios.sim.release
6972

7073
- name: Detox test (ios.sim.release)
7174
env:
7275
RNU_CLI_ROOT: ${{ github.workspace }}/react-native-update-cli
73-
run: cd Example/testHotUpdate && E2E_PLATFORM=ios npx detox test --configuration ios.sim.release
76+
run: cd Example/e2etest && E2E_PLATFORM=ios bunx detox test --configuration ios.sim.release

Example/e2etest/.detoxrc.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,23 @@ function detectAndroidAvdName() {
5858
.split('\n')
5959
.map(line => line.trim())
6060
.filter(Boolean);
61+
62+
const preferredPatterns = [/^api34$/i, /\bapi[_-]?34\b/i, /\b34\b/];
63+
for (const pattern of preferredPatterns) {
64+
const preferredAvd = avds.find(item => pattern.test(item));
65+
if (preferredAvd) {
66+
return preferredAvd;
67+
}
68+
}
69+
6170
if (avds.length > 0) {
6271
return avds[0];
6372
}
6473
} catch {
6574
// fall through to default
6675
}
6776

68-
return 'Pixel_3a_API_33_arm64-v8a';
77+
return 'api34';
6978
}
7079

7180
const iosSimulatorType = detectIosSimulatorType();

Example/e2etest/android/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ android {
109109
}
110110

111111

112-
def detoxVersion = "20.41.2"
112+
def detoxVersion = "20.50.1"
113113

114114
repositories {
115115
maven {

Example/e2etest/e2e/local-merge.test.ts

Lines changed: 140 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ import { LOCAL_UPDATE_HASHES, LOCAL_UPDATE_LABELS } from './localUpdateConfig.ts
44
const RELOAD_TIMEOUT = 180000;
55
const MARK_SUCCESS_TIMEOUT = 30000;
66
const MARK_SUCCESS_SETTLE_MS = 1500;
7+
const DOWNLOAD_SUCCESS_TIMEOUT = 120000;
8+
9+
function getDetoxLaunchArgs() {
10+
if (device.getPlatform() !== 'android') {
11+
return {};
12+
}
13+
14+
return {
15+
launchArgs: {
16+
detoxEnableSynchronization: '0',
17+
},
18+
};
19+
}
20+
21+
async function relaunchAppPreservingData() {
22+
await device.launchApp({
23+
newInstance: true,
24+
...getDetoxLaunchArgs(),
25+
});
26+
}
727

828
async function tapCheckUpdate() {
929
await waitFor(element(by.id('check-update'))).toBeVisible().withTimeout(30000);
@@ -17,8 +37,9 @@ async function waitForBundleLabel(expectedLabel: string) {
1737
}
1838

1939
async function waitForHash(expectedHash: string) {
40+
const visibleHash = expectedHash || '(empty)';
2041
await waitFor(element(by.id('current-hash')))
21-
.toHaveText(`currentHash: ${expectedHash}`)
42+
.toHaveText(`currentHash: ${visibleHash}`)
2243
.withTimeout(RELOAD_TIMEOUT);
2344
}
2445

@@ -29,27 +50,51 @@ async function waitForMarkSuccess() {
2950
await new Promise(resolve => setTimeout(resolve, MARK_SUCCESS_SETTLE_MS));
3051
}
3152

53+
async function waitForDownloadSuccess(expectedHash: string) {
54+
await waitFor(element(by.id('last-event')))
55+
.toHaveText('lastEvent: downloadSuccess')
56+
.withTimeout(DOWNLOAD_SUCCESS_TIMEOUT);
57+
await waitFor(element(by.id('last-event-version')))
58+
.toHaveText(`lastEventVersion: ${expectedHash}`)
59+
.withTimeout(DOWNLOAD_SUCCESS_TIMEOUT);
60+
}
61+
3262
async function waitForReady() {
3363
await waitFor(element(by.id('check-update'))).toBeVisible().withTimeout(30000);
3464
}
3565

66+
async function waitForCheckState(expectedStatus: string, expectedResult: string) {
67+
await waitFor(element(by.id('last-check-status')))
68+
.toHaveText(`lastCheckStatus: ${expectedStatus}`)
69+
.withTimeout(RELOAD_TIMEOUT);
70+
await waitFor(element(by.id('last-check-result')))
71+
.toHaveText(`lastCheckResult: ${expectedResult}`)
72+
.withTimeout(RELOAD_TIMEOUT);
73+
}
74+
75+
async function waitForStrategy(expectedStrategy: 'silentAndNow' | 'silentAndLater') {
76+
await waitFor(element(by.id('update-strategy')))
77+
.toHaveText(`updateStrategy: ${expectedStrategy}`)
78+
.withTimeout(10000);
79+
}
80+
81+
async function selectStrategy(testId: 'strategy-silent-now' | 'strategy-silent-later') {
82+
await waitFor(element(by.id(testId))).toBeVisible().withTimeout(10000);
83+
await element(by.id(testId)).tap();
84+
}
85+
3686
describe('Local Update Merge E2E', () => {
37-
beforeAll(async () => {
87+
beforeEach(async () => {
3888
await device.launchApp({
3989
newInstance: true,
4090
delete: true,
41-
...(device.getPlatform() === 'android'
42-
? {
43-
launchArgs: {
44-
detoxEnableSynchronization: '0',
45-
},
46-
}
47-
: {}),
91+
...getDetoxLaunchArgs(),
4892
});
4993
});
5094

5195
it('covers local full update, diff merge, and package diff through checkUpdate + silentAndNow', async () => {
5296
await waitForReady();
97+
await waitForStrategy('silentAndNow');
5398
await waitForBundleLabel(LOCAL_UPDATE_LABELS.base);
5499

55100
await tapCheckUpdate();
@@ -68,5 +113,91 @@ describe('Local Update Merge E2E', () => {
68113
await waitForHash(LOCAL_UPDATE_HASHES.packagePatch);
69114
await waitForMarkSuccess();
70115
}
116+
117+
const finalLabel =
118+
device.getPlatform() === 'android'
119+
? LOCAL_UPDATE_LABELS.packagePatch
120+
: LOCAL_UPDATE_LABELS.ppkPatch;
121+
const finalHash =
122+
device.getPlatform() === 'android'
123+
? LOCAL_UPDATE_HASHES.packagePatch
124+
: LOCAL_UPDATE_HASHES.ppkPatch;
125+
126+
await relaunchAppPreservingData();
127+
await waitForReady();
128+
await waitForBundleLabel(finalLabel);
129+
await waitForHash(finalHash);
130+
131+
await tapCheckUpdate();
132+
await waitForCheckState('completed', 'upToDate');
133+
await waitForBundleLabel(finalLabel);
134+
await waitForHash(finalHash);
135+
});
136+
137+
it('covers local full update, deferred install, and follow-up deferred patches through silentAndLater', async () => {
138+
await waitForReady();
139+
await waitForBundleLabel(LOCAL_UPDATE_LABELS.base);
140+
141+
await selectStrategy('strategy-silent-later');
142+
await waitForStrategy('silentAndLater');
143+
144+
await tapCheckUpdate();
145+
await waitForCheckState('completed', `update:${LOCAL_UPDATE_HASHES.full}`);
146+
await waitForDownloadSuccess(LOCAL_UPDATE_HASHES.full);
147+
await waitForBundleLabel(LOCAL_UPDATE_LABELS.base);
148+
await waitForHash('');
149+
150+
await relaunchAppPreservingData();
151+
await waitForReady();
152+
await waitForBundleLabel(LOCAL_UPDATE_LABELS.full);
153+
await waitForHash(LOCAL_UPDATE_HASHES.full);
154+
await waitForMarkSuccess();
155+
156+
await selectStrategy('strategy-silent-later');
157+
await waitForStrategy('silentAndLater');
158+
await tapCheckUpdate();
159+
await waitForCheckState('completed', `update:${LOCAL_UPDATE_HASHES.ppkPatch}`);
160+
await waitForDownloadSuccess(LOCAL_UPDATE_HASHES.ppkPatch);
161+
await waitForBundleLabel(LOCAL_UPDATE_LABELS.full);
162+
await waitForHash(LOCAL_UPDATE_HASHES.full);
163+
164+
await relaunchAppPreservingData();
165+
await waitForReady();
166+
await waitForBundleLabel(LOCAL_UPDATE_LABELS.ppkPatch);
167+
await waitForHash(LOCAL_UPDATE_HASHES.ppkPatch);
168+
await waitForMarkSuccess();
169+
170+
if (device.getPlatform() === 'android') {
171+
await selectStrategy('strategy-silent-later');
172+
await waitForStrategy('silentAndLater');
173+
await tapCheckUpdate();
174+
await waitForCheckState(
175+
'completed',
176+
`update:${LOCAL_UPDATE_HASHES.packagePatch}`,
177+
);
178+
await waitForDownloadSuccess(LOCAL_UPDATE_HASHES.packagePatch);
179+
await waitForBundleLabel(LOCAL_UPDATE_LABELS.ppkPatch);
180+
await waitForHash(LOCAL_UPDATE_HASHES.ppkPatch);
181+
182+
await relaunchAppPreservingData();
183+
await waitForReady();
184+
await waitForBundleLabel(LOCAL_UPDATE_LABELS.packagePatch);
185+
await waitForHash(LOCAL_UPDATE_HASHES.packagePatch);
186+
await waitForMarkSuccess();
187+
}
188+
189+
const finalLabel =
190+
device.getPlatform() === 'android'
191+
? LOCAL_UPDATE_LABELS.packagePatch
192+
: LOCAL_UPDATE_LABELS.ppkPatch;
193+
const finalHash =
194+
device.getPlatform() === 'android'
195+
? LOCAL_UPDATE_HASHES.packagePatch
196+
: LOCAL_UPDATE_HASHES.ppkPatch;
197+
198+
await tapCheckUpdate();
199+
await waitForCheckState('completed', 'upToDate');
200+
await waitForBundleLabel(finalLabel);
201+
await waitForHash(finalHash);
71202
});
72203
});

0 commit comments

Comments
 (0)