18d307f52SEvan Bacon/* eslint-env jest */ 21ae005aaSWill Schurmanimport { 31ae005aaSWill Schurman isMultipartPartWithName, 41ae005aaSWill Schurman parseMultipartMixedResponseAsync, 51ae005aaSWill Schurman} from '@expo/multipart-body-parser'; 68d307f52SEvan Baconimport execa from 'execa'; 7e377ff85SWill Schurmanimport fs from 'fs-extra'; 88d307f52SEvan Baconimport fetch from 'node-fetch'; 91ae005aaSWill Schurmanimport nullthrows from 'nullthrows'; 10e377ff85SWill Schurmanimport path from 'path'; 11e377ff85SWill Schurman 120a6ddb20SEvan Baconimport { 130a6ddb20SEvan Bacon execute, 140a6ddb20SEvan Bacon projectRoot, 150a6ddb20SEvan Bacon getLoadedModulesAsync, 160a6ddb20SEvan Bacon setupTestProjectAsync, 170a6ddb20SEvan Bacon bin, 180a6ddb20SEvan Bacon ensurePortFreeAsync, 190a6ddb20SEvan Bacon} from './utils'; 208d307f52SEvan Bacon 218d307f52SEvan Baconconst originalForceColor = process.env.FORCE_COLOR; 228d307f52SEvan Baconconst originalCI = process.env.CI; 238d307f52SEvan Bacon 248d307f52SEvan BaconbeforeAll(async () => { 258d307f52SEvan Bacon await fs.mkdir(projectRoot, { recursive: true }); 268d307f52SEvan Bacon process.env.FORCE_COLOR = '0'; 278d307f52SEvan Bacon process.env.CI = '1'; 288d307f52SEvan Bacon}); 298d307f52SEvan Bacon 308d307f52SEvan BaconafterAll(() => { 318d307f52SEvan Bacon process.env.FORCE_COLOR = originalForceColor; 328d307f52SEvan Bacon process.env.CI = originalCI; 338d307f52SEvan Bacon}); 348d307f52SEvan Bacon 358d307f52SEvan Baconit('loads expected modules by default', async () => { 368d307f52SEvan Bacon const modules = await getLoadedModulesAsync(`require('../../build/src/start').expoStart`); 378d307f52SEvan Bacon expect(modules).toStrictEqual([ 384067174dSWill Schurman '../node_modules/ansi-styles/index.js', 398d307f52SEvan Bacon '../node_modules/arg/index.js', 408d307f52SEvan Bacon '../node_modules/chalk/source/index.js', 418d307f52SEvan Bacon '../node_modules/chalk/source/util.js', 428d307f52SEvan Bacon '../node_modules/has-flag/index.js', 438d307f52SEvan Bacon '../node_modules/supports-color/index.js', 448d307f52SEvan Bacon '@expo/cli/build/src/log.js', 458d307f52SEvan Bacon '@expo/cli/build/src/start/index.js', 468d307f52SEvan Bacon '@expo/cli/build/src/utils/args.js', 478d307f52SEvan Bacon '@expo/cli/build/src/utils/errors.js', 488d307f52SEvan Bacon ]); 498d307f52SEvan Bacon}); 508d307f52SEvan Bacon 518d307f52SEvan Baconit('runs `npx expo start --help`', async () => { 528d307f52SEvan Bacon const results = await execute('start', '--help'); 538d307f52SEvan Bacon expect(results.stdout).toMatchInlineSnapshot(` 548d307f52SEvan Bacon " 5583d464dcSEvan Bacon Info 568d307f52SEvan Bacon Start a local dev server for the app 578d307f52SEvan Bacon 588d307f52SEvan Bacon Usage 598d307f52SEvan Bacon $ npx expo start <dir> 608d307f52SEvan Bacon 618d307f52SEvan Bacon Options 6283d464dcSEvan Bacon <dir> Directory of the Expo project. Default: Current working directory 6341c91838SEvan Bacon -a, --android Open on a connected Android device 6441c91838SEvan Bacon -i, --ios Open in an iOS simulator 6541c91838SEvan Bacon -w, --web Open in a web browser 6641c91838SEvan Bacon 6741c91838SEvan Bacon -d, --dev-client Launch in a custom native app 6841c91838SEvan Bacon -g, --go Launch in Expo Go 698d307f52SEvan Bacon 708d307f52SEvan Bacon -c, --clear Clear the bundler cache 7141c91838SEvan Bacon --max-workers <number> Maximum number of tasks to allow Metro to spawn 728d307f52SEvan Bacon --no-dev Bundle in production mode 738d307f52SEvan Bacon --minify Minify JavaScript 748d307f52SEvan Bacon 75d7ad395fSEvan Bacon -m, --host <string> Dev server hosting type. Default: lan 7683d464dcSEvan Bacon lan: Use the local network 7783d464dcSEvan Bacon tunnel: Use any network by tunnel through ngrok 7883d464dcSEvan Bacon localhost: Connect to the dev server over localhost 798d307f52SEvan Bacon --tunnel Same as --host tunnel 808d307f52SEvan Bacon --lan Same as --host lan 818d307f52SEvan Bacon --localhost Same as --host localhost 828d307f52SEvan Bacon 838d307f52SEvan Bacon --offline Skip network requests and use anonymous manifest signatures 848d307f52SEvan Bacon --https Start the dev server with https protocol 858d307f52SEvan Bacon --scheme <scheme> Custom URI protocol to use when launching an app 8647d62600SKudo Chien -p, --port <number> Port to start the dev server on (does not apply to web or tunnel). Default: 8081 878d307f52SEvan Bacon 88e1bb5bdfSKudo Chien --private-key-path <path> Path to private key for code signing. Default: "private-key.pem" in the same directory as the certificate specified by the expo-updates configuration in app.json. 8983d464dcSEvan Bacon -h, --help Usage info 908d307f52SEvan Bacon " 918d307f52SEvan Bacon `); 928d307f52SEvan Bacon}); 938d307f52SEvan Bacon 948d307f52SEvan Baconfor (const args of [ 958d307f52SEvan Bacon ['--lan', '--tunnel'], 968d307f52SEvan Bacon ['--offline', '--localhost'], 978d307f52SEvan Bacon ['--host', 'localhost', '--tunnel'], 988d307f52SEvan Bacon ['-m', 'localhost', '--lan', '--offline'], 998d307f52SEvan Bacon]) { 1008d307f52SEvan Bacon it(`asserts invalid URL arguments on \`expo start ${args.join(' ')}\``, async () => { 1018d307f52SEvan Bacon await expect(execa('node', [bin, 'start', ...args], { cwd: projectRoot })).rejects.toThrowError( 1028d307f52SEvan Bacon /Specify at most one of/ 1038d307f52SEvan Bacon ); 1048d307f52SEvan Bacon }); 1058d307f52SEvan Bacon} 1068d307f52SEvan Bacon 1070a6ddb20SEvan Bacondescribe('server', () => { 1080a6ddb20SEvan Bacon // Kill port 10947d62600SKudo Chien const kill = () => ensurePortFreeAsync(8081); 1100a6ddb20SEvan Bacon 1110a6ddb20SEvan Bacon beforeEach(async () => { 1120a6ddb20SEvan Bacon await kill(); 1130a6ddb20SEvan Bacon }); 1140a6ddb20SEvan Bacon 1150a6ddb20SEvan Bacon afterAll(async () => { 1160a6ddb20SEvan Bacon await kill(); 1170a6ddb20SEvan Bacon }); 1188d307f52SEvan Bacon it( 1198d307f52SEvan Bacon 'runs `npx expo start`', 1208d307f52SEvan Bacon async () => { 1218d307f52SEvan Bacon const projectRoot = await setupTestProjectAsync('basic-start', 'with-blank'); 1228d307f52SEvan Bacon await fs.remove(path.join(projectRoot, '.expo')); 1238d307f52SEvan Bacon 1248d307f52SEvan Bacon const promise = execa('node', [bin, 'start'], { cwd: projectRoot }); 1258d307f52SEvan Bacon 1268d307f52SEvan Bacon console.log('Starting server'); 1278d307f52SEvan Bacon 1288d307f52SEvan Bacon await new Promise<void>((resolve, reject) => { 1298d307f52SEvan Bacon promise.on('close', (code: number) => { 1308d307f52SEvan Bacon reject( 1318d307f52SEvan Bacon code === 0 13247d62600SKudo Chien ? 'Server closed too early. Run `kill -9 $(lsof -ti:8081)` to kill the orphaned process.' 1338d307f52SEvan Bacon : code 1348d307f52SEvan Bacon ); 1358d307f52SEvan Bacon }); 1368d307f52SEvan Bacon 137e1bb5bdfSKudo Chien promise.stdout?.on('data', (data) => { 1388d307f52SEvan Bacon const stdout = data.toString(); 1398d307f52SEvan Bacon console.log('output:', stdout); 1408d307f52SEvan Bacon if (stdout.includes('Logs for your project')) { 1418d307f52SEvan Bacon resolve(); 1428d307f52SEvan Bacon } 1438d307f52SEvan Bacon }); 1448d307f52SEvan Bacon }); 1458d307f52SEvan Bacon 1468d307f52SEvan Bacon console.log('Fetching manifest'); 14747d62600SKudo Chien const response = await fetch('http://localhost:8081/', { 1486d6b81f9SEvan Bacon headers: { 1496d6b81f9SEvan Bacon 'expo-platform': 'ios', 1501ae005aaSWill Schurman Accept: 'multipart/mixed', 1516d6b81f9SEvan Bacon }, 1521ae005aaSWill Schurman }); 1531ae005aaSWill Schurman 1541ae005aaSWill Schurman const multipartParts = await parseMultipartMixedResponseAsync( 1551ae005aaSWill Schurman response.headers.get('content-type') as string, 1561ae005aaSWill Schurman await response.buffer() 1571ae005aaSWill Schurman ); 1581ae005aaSWill Schurman const manifestPart = nullthrows( 1591ae005aaSWill Schurman multipartParts.find((part) => isMultipartPartWithName(part, 'manifest')) 1601ae005aaSWill Schurman ); 1611ae005aaSWill Schurman 1621ae005aaSWill Schurman const manifest = JSON.parse(manifestPart.body); 1638d307f52SEvan Bacon 1648d307f52SEvan Bacon // Required for Expo Go 1651ae005aaSWill Schurman expect(manifest.extra.expoGo.packagerOpts).toStrictEqual({ 1668d307f52SEvan Bacon dev: true, 1678d307f52SEvan Bacon }); 1681ae005aaSWill Schurman expect(manifest.extra.expoGo.developer).toStrictEqual({ 1698d307f52SEvan Bacon projectRoot: expect.anything(), 1708d307f52SEvan Bacon tool: 'expo-cli', 1718d307f52SEvan Bacon }); 1728d307f52SEvan Bacon 1738d307f52SEvan Bacon // URLs 1741ae005aaSWill Schurman expect(manifest.launchAsset.url).toBe( 175*465d3694SEvan Bacon 'http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&hot=false' 1768d307f52SEvan Bacon ); 17747d62600SKudo Chien expect(manifest.extra.expoGo.debuggerHost).toBe('127.0.0.1:8081'); 1781ae005aaSWill Schurman expect(manifest.extra.expoGo.mainModuleName).toBe('node_modules/expo/AppEntry'); 17947d62600SKudo Chien expect(manifest.extra.expoClient.hostUri).toBe('127.0.0.1:8081'); 1808d307f52SEvan Bacon 1818d307f52SEvan Bacon // Manifest 1821ae005aaSWill Schurman expect(manifest.runtimeVersion).toBe('exposdk:47.0.0'); 1831ae005aaSWill Schurman expect(manifest.extra.expoClient.sdkVersion).toBe('47.0.0'); 1841ae005aaSWill Schurman expect(manifest.extra.expoClient.slug).toBe('basic-start'); 1851ae005aaSWill Schurman expect(manifest.extra.expoClient.name).toBe('basic-start'); 1868d307f52SEvan Bacon 1878d307f52SEvan Bacon // Custom 1881ae005aaSWill Schurman expect(manifest.extra.expoGo.__flipperHack).toBe('React Native packager is running'); 1898d307f52SEvan Bacon 1908d307f52SEvan Bacon console.log('Fetching bundle'); 1911ae005aaSWill Schurman const bundle = await fetch(manifest.launchAsset.url).then((res) => res.text()); 1928d307f52SEvan Bacon console.log('Fetched bundle: ', bundle.length); 1938d307f52SEvan Bacon expect(bundle.length).toBeGreaterThan(1000); 1948d307f52SEvan Bacon console.log('Finished'); 1958d307f52SEvan Bacon 1968d307f52SEvan Bacon // Kill process. 197e1bb5bdfSKudo Chien promise.kill('SIGTERM'); 1988d307f52SEvan Bacon 1998d307f52SEvan Bacon await promise; 2008d307f52SEvan Bacon }, 2018d307f52SEvan Bacon // Could take 45s depending on how fast npm installs 2028d307f52SEvan Bacon 120 * 1000 2038d307f52SEvan Bacon ); 2040a6ddb20SEvan Bacon}); 205