Skip to content

Commit 93117ea

Browse files
cipolleschifacebook-github-bot
authored andcommitted
Move E2E scripts to js (#48419)
Summary: Pull Request resolved: #48419 This change moves the E2E scripts for iOS to a JS script. This should make it much easier to modify the code in case we need to change it. ## Changelog [Internal] - Move e2e script from bash to JS Reviewed By: cortinico Differential Revision: D67737950 fbshipit-source-id: d0b0411c8a4d688c10e460e70b11dbfc83aaa135
1 parent 09995fc commit 93117ea

2 files changed

Lines changed: 174 additions & 46 deletions

File tree

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

Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -57,52 +57,13 @@ runs:
5757
# Maestro can fail in case of flakyness, we have some retry logic.
5858
set +e
5959
60-
echo "Launching iOS Simulator: iPhone 15 Pro"
61-
xcrun simctl boot "iPhone 15 Pro"
62-
63-
echo "Installing app on Simulator"
64-
xcrun simctl install booted "${{ inputs.app-path }}"
65-
66-
echo "Retrieving device UDID"
67-
UDID=$(xcrun simctl list devices booted -j | jq -r '[.devices[]] | add | first | .udid')
68-
echo "UDID is $UDID"
69-
70-
echo "Bring simulator in foreground"
71-
open -a simulator
72-
73-
echo "Launch the app"
74-
xcrun simctl launch $UDID ${{ inputs.app-id }}
75-
76-
if [[ ${{ inputs.flavor }} == 'Debug' ]]; then
77-
# To give the app time to warm the metro's cache
78-
sleep 20
79-
fi
80-
81-
echo "Running tests with Maestro"
82-
export MAESTRO_DRIVER_STARTUP_TIMEOUT=1500000 # 25 min. CI is extremely slow
83-
84-
# Add retries for flakyness
85-
MAX_ATTEMPTS=5
86-
CURR_ATTEMPT=0
87-
RESULT=1
88-
89-
while [[ $CURR_ATTEMPT -lt $MAX_ATTEMPTS ]] && [[ $RESULT -ne 0 ]]; do
90-
CURR_ATTEMPT=$((CURR_ATTEMPT+1))
91-
echo "Attempt number $CURR_ATTEMPT"
92-
93-
echo "Start video record using pid: video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid"
94-
xcrun simctl io booted recordVideo video_record_$CURR_ATTEMPT.mov & echo $! > video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid
95-
96-
echo '$HOME/.maestro/bin/maestro --udid=$UDID test ${{ inputs.maestro-flow }} --format junit -e APP_ID=${{ inputs.app-id }}'
97-
$HOME/.maestro/bin/maestro --udid=$UDID test ${{ inputs.maestro-flow }} --format junit -e APP_ID=${{ inputs.app-id }} --debug-output /tmp/MaestroLogs
98-
99-
RESULT=$?
100-
101-
# Stop video
102-
kill -SIGINT $(cat video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid)
103-
done
104-
105-
exit $RESULT
60+
node .github/workflow-scripts/maestro-ios.js \
61+
"${{ inputs.app-path }}" \
62+
"${{ inputs.app-id }}" \
63+
"${{ inputs.maestro-flow }}" \
64+
"${{ inputs.jsengine }}" \
65+
"${{ inputs.flavor }}" \
66+
"${{ inputs.working-directory }}"
10667
- name: Store video record
10768
if: always()
10869
uses: actions/upload-artifact@v4.3.4
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const childProcess = require('child_process');
11+
const fs = require('fs');
12+
13+
const usage = `
14+
=== Usage ===
15+
node maestro-android.js <path to app> <app_id> <maestro_flow> <flavor> <working_directory>
16+
17+
@param {string} appPath - Path to the app APK
18+
@param {string} appId - App ID that needs to be launched
19+
@param {string} maestroFlow - Path to the maestro flow to be executed
20+
@param {string} jsengine - The JSEngine to use for the test
21+
@param {string} flavor - Flavor of the app to be launched. Can be 'Release' or 'Debug'
22+
@param {string} workingDirectory - Working directory from where to run Metro
23+
==============
24+
`;
25+
26+
const args = process.argv.slice(2);
27+
28+
if (args.length !== 6) {
29+
throw new Error(`Invalid number of arguments.\n${usage}`);
30+
}
31+
32+
const APP_PATH = args[0];
33+
const APP_ID = args[1];
34+
const MAESTRO_FLOW = args[2];
35+
const JS_ENGINE = args[3];
36+
const IS_DEBUG = args[4] === 'Debug';
37+
const WORKING_DIRECTORY = args[5];
38+
39+
const MAX_ATTEMPTS = 5;
40+
41+
function launchSimulator(simulatorName) {
42+
console.log(`Launching simulator ${simulatorName}`);
43+
try {
44+
childProcess.execSync(`xcrun simctl boot "${simulatorName}"`);
45+
} catch (error) {
46+
if (
47+
!error.message.includes('Unable to boot device in current state: Booted')
48+
) {
49+
throw error;
50+
}
51+
}
52+
}
53+
54+
function installAppOnSimulator(appPath) {
55+
console.log(`Installing app at path ${appPath}`);
56+
childProcess.execSync(`xcrun simctl install booted "${appPath}"`);
57+
}
58+
59+
function extractSimulatorUDID() {
60+
console.log('Retrieving device UDID');
61+
const command = `xcrun simctl list devices booted -j | jq -r '[.devices[]] | add | first | .udid'`;
62+
const udid = String(childProcess.execSync(command)).trim();
63+
console.log(`UDID is ${udid}`);
64+
return udid;
65+
}
66+
67+
function bringSimulatorInForeground() {
68+
console.log('Bringing simulator in foreground');
69+
childProcess.execSync('open -a simulator');
70+
}
71+
72+
function sleep(ms) {
73+
return new Promise(resolve => {
74+
setTimeout(resolve, ms);
75+
});
76+
}
77+
78+
async function launchAppOnSimulator(appId, udid, isDebug) {
79+
console.log('Launch the app');
80+
childProcess.execSync(`xcrun simctl launch "${udid}" "${appId}"`);
81+
82+
if (isDebug) {
83+
console.log('Wait for metro to warm');
84+
await sleep(20 * 1000);
85+
}
86+
}
87+
88+
function startVideoRecording(jsengine, currentAttempt) {
89+
console.log(
90+
`Start video record using pid: video_record_${jsengine}_${currentAttempt}.pid`,
91+
);
92+
childProcess.exec(
93+
`xcrun simctl io booted recordVideo video_record_${jsengine}_${currentAttempt}.mov & echo $! > video_record_${jsengine}_${currentAttempt}.pid`,
94+
);
95+
}
96+
97+
function stopVideoRecording(jsengine, currentAttempt) {
98+
console.log(
99+
`Stop video record using pid: video_record_${jsengine}_${currentAttempt}.pid`,
100+
);
101+
childProcess.exec(
102+
`kill -SIGINT $(cat video_record_${jsengine}_${currentAttempt}.pid)`,
103+
);
104+
}
105+
106+
function executeTestsWithRetries(
107+
appId,
108+
udid,
109+
maestroFlow,
110+
jsengine,
111+
currentAttempt,
112+
) {
113+
try {
114+
startVideoRecording(jsengine, currentAttempt);
115+
116+
const timeout = 1000 * 60 * 10; // 10 minutes
117+
const command = `$HOME/.maestro/bin/maestro --udid="${udid}" test "${maestroFlow}" --format junit -e APP_ID="${appId}"`;
118+
console.log(command);
119+
childProcess.execSync(`MAESTRO_DRIVER_STARTUP_TIMEOUT=1500000 ${command}`, {
120+
stdio: 'inherit',
121+
timeout,
122+
});
123+
124+
stopVideoRecording(jsengine, currentAttempt);
125+
} catch (error) {
126+
// Can't put this in the finally block because it will be executed after the
127+
// recursive call of executeTestsWithRetries
128+
stopVideoRecording(jsengine, currentAttempt);
129+
130+
if (currentAttempt < MAX_ATTEMPTS) {
131+
executeTestsWithRetries(
132+
appId,
133+
udid,
134+
maestroFlow,
135+
jsengine,
136+
currentAttempt + 1,
137+
);
138+
} else {
139+
console.error(`Failed to execute flow after ${MAX_ATTEMPTS} attempts.`);
140+
throw error;
141+
}
142+
}
143+
}
144+
145+
async function main() {
146+
console.info('\n==============================');
147+
console.info('Running tests for iOS with the following parameters:');
148+
console.info(`APP_PATH: ${APP_PATH}`);
149+
console.info(`APP_ID: ${APP_ID}`);
150+
console.info(`MAESTRO_FLOW: ${MAESTRO_FLOW}`);
151+
console.info(`JS_ENGINE: ${JS_ENGINE}`);
152+
console.info(`IS_DEBUG: ${IS_DEBUG}`);
153+
console.info(`WORKING_DIRECTORY: ${WORKING_DIRECTORY}`);
154+
console.info('==============================\n');
155+
156+
const simulatorName = 'iPhone 15 Pro';
157+
launchSimulator(simulatorName);
158+
installAppOnSimulator(APP_PATH);
159+
const udid = extractSimulatorUDID();
160+
bringSimulatorInForeground();
161+
await launchAppOnSimulator(APP_ID, udid, IS_DEBUG);
162+
executeTestsWithRetries(APP_ID, udid, MAESTRO_FLOW, JS_ENGINE, 1);
163+
console.log('Test finished');
164+
process.exit(0);
165+
}
166+
167+
main();

0 commit comments

Comments
 (0)