Skip to content

Commit ea06a31

Browse files
cortinicometa-codesync[bot]
authored andcommitted
Stabilize Android E2E tests for ARM64 APKs (#58140)
Summary: Stabilizes the Android RNTester E2E jobs after switching their APKs to ARM64 while continuing to run the emulator on an x86_64 host. - Updates the Android wide-gamut screenshot baseline using the stable ARM64 result from the API 35 emulator. The emulator does not support wide color, so the Display-P3 fixture is converted to solid sRGB red. - Marks the FlatList `maintainVisibleContentPosition` flows as Android release-only. These flows remain fully covered by the release APK, where they consistently pass, while avoiding debug-only timing failures caused by running the ARM64 debug runtime through native translation. - Adds generic tag filtering to the Android Maestro runner and unit coverage for it. Across seven post-migration `main` runs, the release APK passed every FlatList flow, while the debug APK consistently dropped or delayed Maestro interactions and skipped up to 172 frames during startup. ## Changelog: [INTERNAL] [FIXED] - Stabilize Android RNTester E2E tests when running ARM64 APKs on x86_64 emulators. Pull Request resolved: #58140 Test Plan: - `./node_modules/.bin/jest .github/workflow-scripts/__tests__/maestro-android-test.js --runInBand --config='{"testEnvironment":"node","transform":{},"roots":["<rootDir>/.github/workflow-scripts"]}'` — passed (3 tests) - `maestro 2.6.1 check-syntax` for all 24 tagged FlatList flows — passed - Prettier check for all changed text files — passed - `git diff --check` — passed - Verified the filtered RNTester suite contains 16 debug flows and excludes 24 release-only FlatList MVCP flows - Compared the new baseline against ARM64 CI screenshots: exact match for release; debug RMSE 0.0024 - Manually exercised the ARM64 release APK on an API 35 ARM64 emulator at 320×640: FlatList offsets progressed as expected (`500 → 544 → 2744 → 4944 → 7144`), and five rapid prepends reached `11500` Related failing run: https://github.com/react/react-native/actions/runs/32821468795 Reviewed By: Abbondanzo Differential Revision: D117361459 Pulled By: cortinico fbshipit-source-id: 4514f8c42f7599c875672efe3c56aae7c9f0395c
1 parent 14184ec commit ea06a31

30 files changed

Lines changed: 125 additions & 6 deletions

.github/actions/maestro-android/action.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ inputs:
3838
required: false
3939
default: /tmp/maestro-android-state/results.json
4040
description: The path used to persist per-flow test results between retries
41+
exclude-tags:
42+
required: false
43+
default: ''
44+
description: Comma-separated flow tags to exclude from the test run
4145

4246
runs:
4347
using: composite
@@ -77,7 +81,7 @@ runs:
7781
cores: '4'
7882
disable-animations: false
7983
avd-name: e2e_emulator
80-
script: node .github/workflow-scripts/maestro-android.js ${{ inputs.app-path }} ${{ inputs.app-id }} ${{ inputs.maestro-flow }} ${{ inputs.flavor }} ${{ inputs.working-directory }} ${{ inputs.test-state-path }}
84+
script: node .github/workflow-scripts/maestro-android.js ${{ inputs.app-path }} ${{ inputs.app-id }} ${{ inputs.maestro-flow }} ${{ inputs.flavor }} ${{ inputs.working-directory }} ${{ inputs.test-state-path }} "${{ inputs.exclude-tags }}"
8185
- name: Normalize APP_ID
8286
id: normalize-app-id
8387
shell: bash

.github/workflow-scripts/__tests__/maestro-android-test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const path = require('path');
1414
const {
1515
collectFlows,
1616
executeFlowSuite,
17+
filterFlowsByTags,
1718
loadState,
1819
} = require('../maestro-android');
1920

@@ -45,6 +46,26 @@ describe('Maestro Android runner', () => {
4546
]);
4647
});
4748

49+
it('excludes flows with matching tags', () => {
50+
const releaseOnlyFlow = path.join(
51+
temporaryDirectory,
52+
'android-release-only.yml',
53+
);
54+
const regularFlow = path.join(temporaryDirectory, 'regular.yml');
55+
fs.writeFileSync(
56+
releaseOnlyFlow,
57+
'appId: x\ntags:\n - android-release-only\n---\n- launchApp\n',
58+
);
59+
fs.writeFileSync(regularFlow, 'appId: x\n---\n- launchApp\n');
60+
61+
expect(
62+
filterFlowsByTags(
63+
[releaseOnlyFlow, regularFlow],
64+
['android-release-only'],
65+
),
66+
).toEqual([regularFlow]);
67+
});
68+
4869
it('runs every flow and retries only flows that have not passed', () => {
4970
const flows = ['first.yml', 'second.yml', 'third.yml'].map(file =>
5071
path.join(temporaryDirectory, file),

.github/workflow-scripts/maestro-android.js

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ const path = require('path');
1313

1414
const usage = `
1515
=== Usage ===
16-
node maestro-android.js <path to app> <app_id> <maestro_flow> <flavor> <working_directory> [test_state_path]
16+
node maestro-android.js <path to app> <app_id> <maestro_flow> <flavor> <working_directory> [test_state_path] [exclude_tags]
1717
1818
@param {string} appPath - Path to the app APK
1919
@param {string} appId - App ID that needs to be launched
2020
@param {string} maestroFlow - Path to the Maestro flow or folder to execute
2121
@param {string} flavor - Flavor of the app to be launched. Can be 'release' or 'debug'
2222
@param {string} workingDirectory - Working directory from where to run Metro
2323
@param {string} testStatePath - File used to persist per-flow results between CI retries
24+
@param {string} excludeTags - Comma-separated flow tags to exclude
2425
==============
2526
`;
2627

@@ -63,6 +64,39 @@ function collectFlows(flowPath) {
6364
return flows;
6465
}
6566

67+
function getFlowTags(flow) {
68+
const contents = fs.readFileSync(flow, 'utf8');
69+
const tags = [];
70+
const tagBlockPattern = /^tags:\s*\r?\n((?:^[ \t]+-[ \t]*[^\r\n]+\r?\n?)*)/gm;
71+
72+
for (const match of contents.matchAll(tagBlockPattern)) {
73+
for (const line of match[1].split(/\r?\n/)) {
74+
const tag = line.match(/^[ \t]+-[ \t]*(.+?)\s*$/)?.[1];
75+
if (tag != null) {
76+
tags.push(tag);
77+
}
78+
}
79+
}
80+
81+
return tags;
82+
}
83+
84+
function filterFlowsByTags(flows, excludeTags) {
85+
if (excludeTags.length === 0) {
86+
return flows;
87+
}
88+
89+
return flows.filter(flow => {
90+
const tags = getFlowTags(flow);
91+
const excludedTag = tags.find(tag => excludeTags.includes(tag));
92+
if (excludedTag != null) {
93+
console.info(`Skipping flow tagged ${excludedTag}: ${flow}`);
94+
return false;
95+
}
96+
return true;
97+
});
98+
}
99+
66100
function getFlowKey(flow) {
67101
return path
68102
.relative(process.cwd(), path.resolve(flow))
@@ -274,7 +308,7 @@ function executeFlowSuite({
274308
}
275309

276310
async function main(args = process.argv.slice(2)) {
277-
if (args.length < 5 || args.length > 6) {
311+
if (args.length < 5 || args.length > 7) {
278312
throw new Error(`Invalid number of arguments.\n${usage}`);
279313
}
280314

@@ -284,6 +318,10 @@ async function main(args = process.argv.slice(2)) {
284318
const isDebug = args[3] === 'debug';
285319
const workingDirectory = args[4];
286320
const statePath = args[5] ?? DEFAULT_STATE_PATH;
321+
const excludeTags = (args[6] ?? '')
322+
.split(',')
323+
.map(tag => tag.trim())
324+
.filter(Boolean);
287325

288326
console.info('\n==============================');
289327
console.info('Running tests for Android with the following parameters:');
@@ -293,6 +331,7 @@ async function main(args = process.argv.slice(2)) {
293331
console.info(`IS_DEBUG: ${isDebug}`);
294332
console.info(`WORKING_DIRECTORY: ${workingDirectory}`);
295333
console.info(`TEST_STATE_PATH: ${statePath}`);
334+
console.info(`EXCLUDE_TAGS: ${excludeTags.join(',') || '<none>'}`);
296335
console.info('==============================\n');
297336

298337
logAndroidAbiConfiguration();
@@ -338,7 +377,7 @@ async function main(args = process.argv.slice(2)) {
338377

339378
let error = null;
340379
try {
341-
const flows = collectFlows(maestroFlow);
380+
const flows = filterFlowsByTags(collectFlows(maestroFlow), excludeTags);
342381
const state = loadState(statePath);
343382
console.info(`Start testing ${flows.length} flow(s)`);
344383
executeFlowSuite({flows, appId, state, statePath});
@@ -378,6 +417,7 @@ if (require.main === module) {
378417
module.exports = {
379418
collectFlows,
380419
executeFlowSuite,
420+
filterFlowsByTags,
381421
formatResults,
382422
loadState,
383423
};

.github/workflows/e2e-android-rntester.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ jobs:
7171
emulator-api-level: '35'
7272
emulator-target: google_apis
7373
emulator-arch: x86_64
74+
# These timing-sensitive flows remain covered by the release APK.
75+
# Running the debug ARM64 APK through native translation can delay
76+
# React commits long enough for Maestro to miss taps and assertions.
77+
exclude-tags: ${{ matrix.flavor == 'debug' && 'android-release-only' || '' }}
7478
- name: Store per-flow test state
7579
if: always()
7680
uses: actions/upload-artifact@v6

packages/rn-tester/.maestro/flatlist-append-maintainvisible.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Test FlatList maintainVisibleContentPosition with append (baseline)
22
# Appending items should NOT affect scroll offset (delta ~0)
33
appId: ${APP_ID}
4+
tags:
5+
- android-release-only
46
---
57
- launchApp
68
# Change to portrait

packages/rn-tester/.maestro/flatlist-complex-mutations-maintainvisible.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Test FlatList maintainVisibleContentPosition — complex concurrent mutations
22
# Tests prepend + append + delete in sequence
33
appId: ${APP_ID}
4+
tags:
5+
- android-release-only
46
---
57
- launchApp
68
- setOrientation: portrait

packages/rn-tester/.maestro/flatlist-delete-anchor-maintainvisible.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Test FlatList maintainVisibleContentPosition — delete anchor item
22
# When the anchor item (first visible) is deleted, MVCP should select a new anchor
33
appId: ${APP_ID}
4+
tags:
5+
- android-release-only
46
---
57
- launchApp
68
- setOrientation: portrait

packages/rn-tester/.maestro/flatlist-delete-middle-maintainvisible.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Test FlatList maintainVisibleContentPosition — delete from middle
22
# When items are deleted from the middle, MVCP should adjust scroll offset
33
appId: ${APP_ID}
4+
tags:
5+
- android-release-only
46
---
57
- launchApp
68
- setOrientation: portrait

packages/rn-tester/.maestro/flatlist-empty-list-maintainvisible.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# _firstVisibleView.frame on nil returns {0,0}, causing incorrect scroll
44
# correction.
55
appId: ${APP_ID}
6+
tags:
7+
- android-release-only
68
---
79
- launchApp
810
# Change to portrait

packages/rn-tester/.maestro/flatlist-first-prepend-maintainvisible.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Test FlatList maintainVisibleContentPosition — first prepend only
22
# Single prepend with fixed-height items: delta should be ~44px (40px height + 4px margin)
33
appId: ${APP_ID}
4+
tags:
5+
- android-release-only
46
---
57
- launchApp
68
# Change to portrait

0 commit comments

Comments
 (0)