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'use strict';
11
12/**
13 * This script tests that React Native end to end installation/bootstrap works for different platforms
14 * Available arguments:
15 * --ios - 'react-native init' and check iOS app doesn't redbox
16 * --android - 'react-native init' and check Android app doesn't redbox
17 * --js - 'react-native init' and only check the packager returns a bundle
18 * --skip-cli-install - to skip react-native-cli global installation (for local debugging)
19 * --retries [num] - how many times to retry possible flaky commands: yarn add and running tests, default 1
20 */
21
22const {cd, cp, echo, exec, exit, mv, rm} = require('shelljs');
23const spawn = require('child_process').spawn;
24const argv = require('yargs').argv;
25const path = require('path');
26
27const SCRIPTS = __dirname;
28const ROOT = path.normalize(path.join(__dirname, '..'));
29const tryExecNTimes = require('./try-n-times');
30
31const REACT_NATIVE_TEMP_DIR = exec(
32  'mktemp -d /tmp/react-native-XXXXXXXX',
33).stdout.trim();
34const REACT_NATIVE_APP_DIR = `${REACT_NATIVE_TEMP_DIR}/template`;
35const numberOfRetries = argv.retries || 1;
36let SERVER_PID;
37let APPIUM_PID;
38let exitCode;
39
40function describe(message) {
41  echo(`\n\n>>>>> ${message}\n\n\n`);
42}
43
44try {
45  if (argv.android) {
46    describe('Compile Android binaries');
47    if (
48      exec(
49        './gradlew :ReactAndroid:installArchives -Pjobs=1 -Dorg.gradle.jvmargs="-Xmx512m -XX:+HeapDumpOnOutOfMemoryError"',
50      ).code
51    ) {
52      echo('Failed to compile Android binaries');
53      exitCode = 1;
54      throw Error(exitCode);
55    }
56  }
57
58  describe('Create react-native package');
59  if (exec('node ./scripts/set-rn-version.js --version 1000.0.0').code) {
60    echo('Failed to set version and update package.json ready for release');
61    exitCode = 1;
62    throw Error(exitCode);
63  }
64
65  if (exec('npm pack').code) {
66    echo('Failed to pack react-native');
67    exitCode = 1;
68    throw Error(exitCode);
69  }
70
71  const REACT_NATIVE_PACKAGE = path.join(ROOT, 'react-native-*.tgz');
72
73  describe('Scaffold a basic React Native app from template');
74  exec(`rsync -a ${ROOT}/template ${REACT_NATIVE_TEMP_DIR}`);
75  cd(REACT_NATIVE_APP_DIR);
76
77  const METRO_CONFIG = path.join(ROOT, 'metro.config.js');
78  const RN_GET_POLYFILLS = path.join(ROOT, 'rn-get-polyfills.js');
79  const RN_POLYFILLS_PATH = 'packages/polyfills/';
80  exec(`mkdir -p ${RN_POLYFILLS_PATH}`);
81
82  cp(METRO_CONFIG, '.');
83  cp(RN_GET_POLYFILLS, '.');
84  exec(
85    `rsync -a ${ROOT}/${RN_POLYFILLS_PATH} ${REACT_NATIVE_APP_DIR}/${RN_POLYFILLS_PATH}`,
86  );
87  mv('_flowconfig', '.flowconfig');
88  mv('_watchmanconfig', '.watchmanconfig');
89  mv('_bundle', '.bundle');
90
91  describe('Install React Native package');
92  exec(`npm install ${REACT_NATIVE_PACKAGE}`);
93
94  describe('Install node_modules');
95  if (
96    tryExecNTimes(
97      () => {
98        return exec('npm install').code;
99      },
100      numberOfRetries,
101      () => exec('sleep 10s'),
102    )
103  ) {
104    echo('Failed to execute npm install');
105    echo('Most common reason is npm registry connectivity, try again');
106    exitCode = 1;
107    throw Error(exitCode);
108  }
109  exec('rm -rf ./node_modules/react-native/template');
110
111  if (argv.android) {
112    describe('Install end-to-end framework');
113    if (
114      tryExecNTimes(
115        () =>
116          exec(
117            'yarn add --dev [email protected] [email protected] [email protected] [email protected] [email protected]',
118            {silent: true},
119          ).code,
120        numberOfRetries,
121      )
122    ) {
123      echo('Failed to install appium');
124      echo('Most common reason is npm registry connectivity, try again');
125      exitCode = 1;
126      throw Error(exitCode);
127    }
128    cp(`${SCRIPTS}/android-e2e-test.js`, 'android-e2e-test.js');
129    cd('android');
130    describe('Download Maven deps');
131    exec('./gradlew :app:copyDownloadableDepsToLibs');
132    cd('..');
133
134    describe('Generate key');
135    exec('rm android/app/debug.keystore');
136    if (
137      exec(
138        'keytool -genkey -v -keystore android/app/debug.keystore -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US"',
139      ).code
140    ) {
141      echo('Key could not be generated');
142      exitCode = 1;
143      throw Error(exitCode);
144    }
145
146    describe(`Start appium server, ${APPIUM_PID}`);
147    const appiumProcess = spawn('node', ['./node_modules/.bin/appium']);
148    APPIUM_PID = appiumProcess.pid;
149
150    describe('Build the app');
151    if (exec('react-native run-android').code) {
152      echo('could not execute react-native run-android');
153      exitCode = 1;
154      throw Error(exitCode);
155    }
156
157    describe(`Start Metro, ${SERVER_PID}`);
158    // shelljs exec('', {async: true}) does not emit stdout events, so we rely on good old spawn
159    const packagerProcess = spawn('yarn', ['start', '--max-workers 1'], {
160      env: process.env,
161    });
162    SERVER_PID = packagerProcess.pid;
163    // wait a bit to allow packager to startup
164    exec('sleep 15s');
165    describe('Test: Android end-to-end test');
166    if (
167      tryExecNTimes(
168        () => {
169          return exec('node node_modules/.bin/_mocha android-e2e-test.js').code;
170        },
171        numberOfRetries,
172        () => exec('sleep 10s'),
173      )
174    ) {
175      echo('Failed to run Android end-to-end tests');
176      echo('Most likely the code is broken');
177      exitCode = 1;
178      throw Error(exitCode);
179    }
180  }
181
182  if (argv.ios) {
183    cd('ios');
184    // shelljs exec('', {async: true}) does not emit stdout events, so we rely on good old spawn
185    const packagerEnv = Object.create(process.env);
186    packagerEnv.REACT_NATIVE_MAX_WORKERS = 1;
187    describe('Start Metro');
188    const packagerProcess = spawn('yarn', ['start'], {
189      stdio: 'inherit',
190      env: packagerEnv,
191    });
192    SERVER_PID = packagerProcess.pid;
193    exec('sleep 15s');
194    // prepare cache to reduce chances of possible red screen "Can't find variable __fbBatchedBridge..."
195    exec(
196      'response=$(curl --write-out %{http_code} --silent --output /dev/null localhost:8081/index.bundle?platform=ios&dev=true)',
197    );
198    echo(`Metro is running, ${SERVER_PID}`);
199
200    describe('Install CocoaPod dependencies');
201    exec('bundle exec pod install');
202
203    describe('Test: iOS end-to-end test');
204    if (
205      // TODO: Get target OS and simulator from .tests.env
206      tryExecNTimes(
207        () => {
208          return exec(
209            [
210              'xcodebuild',
211              '-workspace',
212              '"HelloWorld.xcworkspace"',
213              '-destination',
214              '"platform=iOS Simulator,name=iPhone 8,OS=13.3"',
215              '-scheme',
216              '"HelloWorld"',
217              '-sdk',
218              'iphonesimulator',
219              '-UseModernBuildSystem=NO',
220              'test',
221            ].join(' ') +
222              ' | ' +
223              [
224                'xcpretty',
225                '--report',
226                'junit',
227                '--output',
228                '"~/react-native/reports/junit/iOS-e2e/results.xml"',
229              ].join(' ') +
230              ' && exit ${PIPESTATUS[0]}',
231          ).code;
232        },
233        numberOfRetries,
234        () => exec('sleep 10s'),
235      )
236    ) {
237      echo('Failed to run iOS end-to-end tests');
238      echo('Most likely the code is broken');
239      exitCode = 1;
240      throw Error(exitCode);
241    }
242    cd('..');
243  }
244
245  if (argv.js) {
246    // Check the packager produces a bundle (doesn't throw an error)
247    describe('Test: Verify packager can generate an Android bundle');
248    if (
249      exec(
250        'yarn react-native bundle --verbose --entry-file index.js --platform android --dev true --bundle-output android-bundle.js --max-workers 1',
251      ).code
252    ) {
253      echo('Could not build Android bundle');
254      exitCode = 1;
255      throw Error(exitCode);
256    }
257    describe('Test: Verify packager can generate an iOS bundle');
258    if (
259      exec(
260        'yarn react-native bundle --entry-file index.js --platform ios --dev true --bundle-output ios-bundle.js --max-workers 1',
261      ).code
262    ) {
263      echo('Could not build iOS bundle');
264      exitCode = 1;
265      throw Error(exitCode);
266    }
267    describe('Test: Flow check');
268    // The resolve package included a test for a malformed package.json (see https://github.com/browserify/resolve/issues/89)
269    // that is failing the flow check. We're removing it.
270    rm('-rf', './node_modules/resolve/test/resolver/malformed_package_json');
271    if (exec(`${ROOT}/node_modules/.bin/flow check`).code) {
272      echo('Flow check failed.');
273      exitCode = 1;
274      throw Error(exitCode);
275    }
276  }
277  exitCode = 0;
278} finally {
279  describe('Clean up');
280  if (SERVER_PID) {
281    echo(`Killing packager ${SERVER_PID}`);
282    exec(`kill -9 ${SERVER_PID}`);
283    // this is quite drastic but packager starts a daemon that we can't kill by killing the parent process
284    // it will be fixed in April (quote David Aurelio), so until then we will kill the zombie by the port number
285    exec("lsof -i tcp:8081 | awk 'NR!=1 {print $2}' | xargs kill");
286  }
287  if (APPIUM_PID) {
288    echo(`Killing appium ${APPIUM_PID}`);
289    exec(`kill -9 ${APPIUM_PID}`);
290  }
291}
292exit(exitCode);
293