mirror of
https://github.com/react/react-native.git
synced 2026-09-28 05:13:06 +08:00
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: https://github.com/react/react-native/pull/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
This commit is contained in:
committed by
meta-codesync[bot]
parent
14184ec643
commit
ea06a3191f
@@ -38,6 +38,10 @@ inputs:
|
||||
required: false
|
||||
default: /tmp/maestro-android-state/results.json
|
||||
description: The path used to persist per-flow test results between retries
|
||||
exclude-tags:
|
||||
required: false
|
||||
default: ''
|
||||
description: Comma-separated flow tags to exclude from the test run
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -77,7 +81,7 @@ runs:
|
||||
cores: '4'
|
||||
disable-animations: false
|
||||
avd-name: e2e_emulator
|
||||
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 }}
|
||||
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 }}"
|
||||
- name: Normalize APP_ID
|
||||
id: normalize-app-id
|
||||
shell: bash
|
||||
|
||||
@@ -14,6 +14,7 @@ const path = require('path');
|
||||
const {
|
||||
collectFlows,
|
||||
executeFlowSuite,
|
||||
filterFlowsByTags,
|
||||
loadState,
|
||||
} = require('../maestro-android');
|
||||
|
||||
@@ -45,6 +46,26 @@ describe('Maestro Android runner', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('excludes flows with matching tags', () => {
|
||||
const releaseOnlyFlow = path.join(
|
||||
temporaryDirectory,
|
||||
'android-release-only.yml',
|
||||
);
|
||||
const regularFlow = path.join(temporaryDirectory, 'regular.yml');
|
||||
fs.writeFileSync(
|
||||
releaseOnlyFlow,
|
||||
'appId: x\ntags:\n - android-release-only\n---\n- launchApp\n',
|
||||
);
|
||||
fs.writeFileSync(regularFlow, 'appId: x\n---\n- launchApp\n');
|
||||
|
||||
expect(
|
||||
filterFlowsByTags(
|
||||
[releaseOnlyFlow, regularFlow],
|
||||
['android-release-only'],
|
||||
),
|
||||
).toEqual([regularFlow]);
|
||||
});
|
||||
|
||||
it('runs every flow and retries only flows that have not passed', () => {
|
||||
const flows = ['first.yml', 'second.yml', 'third.yml'].map(file =>
|
||||
path.join(temporaryDirectory, file),
|
||||
|
||||
@@ -13,7 +13,7 @@ const path = require('path');
|
||||
|
||||
const usage = `
|
||||
=== Usage ===
|
||||
node maestro-android.js <path to app> <app_id> <maestro_flow> <flavor> <working_directory> [test_state_path]
|
||||
node maestro-android.js <path to app> <app_id> <maestro_flow> <flavor> <working_directory> [test_state_path] [exclude_tags]
|
||||
|
||||
@param {string} appPath - Path to the app APK
|
||||
@param {string} appId - App ID that needs to be launched
|
||||
@@ -21,6 +21,7 @@ node maestro-android.js <path to app> <app_id> <maestro_flow> <flavor> <working_
|
||||
@param {string} flavor - Flavor of the app to be launched. Can be 'release' or 'debug'
|
||||
@param {string} workingDirectory - Working directory from where to run Metro
|
||||
@param {string} testStatePath - File used to persist per-flow results between CI retries
|
||||
@param {string} excludeTags - Comma-separated flow tags to exclude
|
||||
==============
|
||||
`;
|
||||
|
||||
@@ -63,6 +64,39 @@ function collectFlows(flowPath) {
|
||||
return flows;
|
||||
}
|
||||
|
||||
function getFlowTags(flow) {
|
||||
const contents = fs.readFileSync(flow, 'utf8');
|
||||
const tags = [];
|
||||
const tagBlockPattern = /^tags:\s*\r?\n((?:^[ \t]+-[ \t]*[^\r\n]+\r?\n?)*)/gm;
|
||||
|
||||
for (const match of contents.matchAll(tagBlockPattern)) {
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const tag = line.match(/^[ \t]+-[ \t]*(.+?)\s*$/)?.[1];
|
||||
if (tag != null) {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
function filterFlowsByTags(flows, excludeTags) {
|
||||
if (excludeTags.length === 0) {
|
||||
return flows;
|
||||
}
|
||||
|
||||
return flows.filter(flow => {
|
||||
const tags = getFlowTags(flow);
|
||||
const excludedTag = tags.find(tag => excludeTags.includes(tag));
|
||||
if (excludedTag != null) {
|
||||
console.info(`Skipping flow tagged ${excludedTag}: ${flow}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function getFlowKey(flow) {
|
||||
return path
|
||||
.relative(process.cwd(), path.resolve(flow))
|
||||
@@ -274,7 +308,7 @@ function executeFlowSuite({
|
||||
}
|
||||
|
||||
async function main(args = process.argv.slice(2)) {
|
||||
if (args.length < 5 || args.length > 6) {
|
||||
if (args.length < 5 || args.length > 7) {
|
||||
throw new Error(`Invalid number of arguments.\n${usage}`);
|
||||
}
|
||||
|
||||
@@ -284,6 +318,10 @@ async function main(args = process.argv.slice(2)) {
|
||||
const isDebug = args[3] === 'debug';
|
||||
const workingDirectory = args[4];
|
||||
const statePath = args[5] ?? DEFAULT_STATE_PATH;
|
||||
const excludeTags = (args[6] ?? '')
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
console.info('\n==============================');
|
||||
console.info('Running tests for Android with the following parameters:');
|
||||
@@ -293,6 +331,7 @@ async function main(args = process.argv.slice(2)) {
|
||||
console.info(`IS_DEBUG: ${isDebug}`);
|
||||
console.info(`WORKING_DIRECTORY: ${workingDirectory}`);
|
||||
console.info(`TEST_STATE_PATH: ${statePath}`);
|
||||
console.info(`EXCLUDE_TAGS: ${excludeTags.join(',') || '<none>'}`);
|
||||
console.info('==============================\n');
|
||||
|
||||
logAndroidAbiConfiguration();
|
||||
@@ -338,7 +377,7 @@ async function main(args = process.argv.slice(2)) {
|
||||
|
||||
let error = null;
|
||||
try {
|
||||
const flows = collectFlows(maestroFlow);
|
||||
const flows = filterFlowsByTags(collectFlows(maestroFlow), excludeTags);
|
||||
const state = loadState(statePath);
|
||||
console.info(`Start testing ${flows.length} flow(s)`);
|
||||
executeFlowSuite({flows, appId, state, statePath});
|
||||
@@ -378,6 +417,7 @@ if (require.main === module) {
|
||||
module.exports = {
|
||||
collectFlows,
|
||||
executeFlowSuite,
|
||||
filterFlowsByTags,
|
||||
formatResults,
|
||||
loadState,
|
||||
};
|
||||
|
||||
@@ -71,6 +71,10 @@ jobs:
|
||||
emulator-api-level: '35'
|
||||
emulator-target: google_apis
|
||||
emulator-arch: x86_64
|
||||
# These timing-sensitive flows remain covered by the release APK.
|
||||
# Running the debug ARM64 APK through native translation can delay
|
||||
# React commits long enough for Maestro to miss taps and assertions.
|
||||
exclude-tags: ${{ matrix.flavor == 'debug' && 'android-release-only' || '' }}
|
||||
- name: Store per-flow test state
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test FlatList maintainVisibleContentPosition with append (baseline)
|
||||
# Appending items should NOT affect scroll offset (delta ~0)
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test FlatList maintainVisibleContentPosition — complex concurrent mutations
|
||||
# Tests prepend + append + delete in sequence
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
- setOrientation: portrait
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test FlatList maintainVisibleContentPosition — delete anchor item
|
||||
# When the anchor item (first visible) is deleted, MVCP should select a new anchor
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
- setOrientation: portrait
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test FlatList maintainVisibleContentPosition — delete from middle
|
||||
# When items are deleted from the middle, MVCP should adjust scroll offset
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
- setOrientation: portrait
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# _firstVisibleView.frame on nil returns {0,0}, causing incorrect scroll
|
||||
# correction.
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test FlatList maintainVisibleContentPosition — first prepend only
|
||||
# Single prepend with fixed-height items: delta should be ~44px (40px height + 4px margin)
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# Verifies that after horizontal mode with 50 prepended items and scroll to 500,
|
||||
# reset returns offset to 0.
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
- setOrientation: portrait
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# Items are 200px wide + 4px margin = 204px each
|
||||
# In inverted mode, prepending adds items to the right end
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test FlatList maintainVisibleContentPosition with view recycling in horizontal + inverted mode
|
||||
# Items are 200px wide + 4px margin = 204px each
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test FlatList maintainVisibleContentPosition in horizontal mode
|
||||
# Horizontal: items are 200px wide, delta should be ~204px (200px + 4px margin)
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test FlatList maintainVisibleContentPosition with view recycling in horizontal mode
|
||||
# Items are 200px wide + 4px margin = 204px each
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# Delta is +44 (same as non-inverted) — frame-based delta measures actual frame shift,
|
||||
# not logical order. Prepending shifts anchor view frame downward in both modes.
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# With windowSize=3, only ~3 pages of items are rendered
|
||||
# In inverted mode, items display in reverse order but MVCP delta behavior is the same
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
# - After prepend: record offset
|
||||
# - Delta should equal height of prepended items (~44px per item with margin)
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
@@ -87,6 +89,8 @@ appId: ${APP_ID}
|
||||
---
|
||||
# Test that user scroll is not interrupted during prepend
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
- assertVisible: 'Components'
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# Verifies that scroll position remains stable after momentum scroll completes
|
||||
# post-prepend. MVCP correction runs asynchronously in didMountItems.
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
# This test verifies that MVCP survives an orientation change and continues
|
||||
# to work correctly after the change.
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# The native side is unaffected by bottom deletes since MVCP only looks at
|
||||
# the first visible view, but re-ordering edge case is untested.
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test FlatList maintainVisibleContentPosition — pull-to-refresh pattern
|
||||
# Simulates scroll-to-top then prepend (like pull-to-refresh with new items)
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
- setOrientation: portrait
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# Exercises the throttle edge case where pendingScrollUpdateCount may not decrement
|
||||
# promptly, blocking render window updates. All prepends fired without waiting.
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# Items are 40px tall + 4px margin = 44px each
|
||||
# With windowSize=3, only ~3 pages of items are rendered
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# during MVCP active causes additive correction (MVCP delta added on top
|
||||
# of the scrollTo target).
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test FlatList maintainVisibleContentPosition with scroll event throttle
|
||||
# Throttle affects timing but not final delta: should be ~44px
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test variable-height items with first prepend
|
||||
# Single prepend with variable height: delta should be 28-112px
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Test FlatList maintainVisibleContentPosition with variable-height items
|
||||
# Delta should be between 28-112px (random height from [30,50,70,90,110])
|
||||
appId: ${APP_ID}
|
||||
tags:
|
||||
- android-release-only
|
||||
---
|
||||
- launchApp
|
||||
# Change to portrait
|
||||
|
||||
@@ -3,8 +3,8 @@ tags:
|
||||
- android-only
|
||||
---
|
||||
# Android-only: alpha transparency + sRGB vs Display-P3 wide-gamut. Color
|
||||
# fidelity needs a screenshot, so we assert load status only. Flaky on the iOS
|
||||
# E2E job, so guarded to Android; iOS skips this flow (empty=pass).
|
||||
# fidelity is covered by a screenshot captured from the ARM64 APK on the API 35
|
||||
# sRGB emulator. The flow is guarded because it is flaky on the iOS E2E job.
|
||||
- runFlow:
|
||||
when:
|
||||
platform: Android
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Reference in New Issue
Block a user