1/* eslint-env jest */ 2import execa from 'execa'; 3import fs from 'fs-extra'; 4import fetch from 'node-fetch'; 5import path from 'path'; 6 7import { execute, projectRoot, getLoadedModulesAsync, setupTestProjectAsync, bin } from './utils'; 8 9const originalForceColor = process.env.FORCE_COLOR; 10const originalCI = process.env.CI; 11 12beforeAll(async () => { 13 await fs.mkdir(projectRoot, { recursive: true }); 14 process.env.FORCE_COLOR = '0'; 15 process.env.CI = '1'; 16}); 17 18afterAll(() => { 19 process.env.FORCE_COLOR = originalForceColor; 20 process.env.CI = originalCI; 21}); 22 23it('loads expected modules by default', async () => { 24 const modules = await getLoadedModulesAsync(`require('../../build/src/start').expoStart`); 25 expect(modules).toStrictEqual([ 26 '../node_modules/ansi-styles/index.js', 27 '../node_modules/arg/index.js', 28 '../node_modules/chalk/source/index.js', 29 '../node_modules/chalk/source/util.js', 30 '../node_modules/has-flag/index.js', 31 '../node_modules/supports-color/index.js', 32 '@expo/cli/build/src/log.js', 33 '@expo/cli/build/src/start/index.js', 34 '@expo/cli/build/src/utils/args.js', 35 '@expo/cli/build/src/utils/errors.js', 36 ]); 37}); 38 39it('runs `npx expo start --help`', async () => { 40 const results = await execute('start', '--help'); 41 expect(results.stdout).toMatchInlineSnapshot(` 42 " 43 Info 44 Start a local dev server for the app 45 46 Usage 47 $ npx expo start <dir> 48 49 Options 50 <dir> Directory of the Expo project. Default: Current working directory 51 -a, --android Opens your app in Expo Go on a connected Android device 52 -i, --ios Opens your app in Expo Go in a currently running iOS simulator on your computer 53 -w, --web Opens your app in a web browser 54 55 -c, --clear Clear the bundler cache 56 --max-workers <num> Maximum number of tasks to allow Metro to spawn 57 --no-dev Bundle in production mode 58 --minify Minify JavaScript 59 60 -m, --host <mode> Dev server hosting type. Default: lan 61 lan: Use the local network 62 tunnel: Use any network by tunnel through ngrok 63 localhost: Connect to the dev server over localhost 64 --tunnel Same as --host tunnel 65 --lan Same as --host lan 66 --localhost Same as --host localhost 67 68 --offline Skip network requests and use anonymous manifest signatures 69 --https Start the dev server with https protocol 70 --scheme <scheme> Custom URI protocol to use when launching an app 71 -p, --port <port> Port to start the dev server on (does not apply to web or tunnel). Default: 19000 72 73 --dev-client Experimental: Starts the bundler for use with the expo-development-client 74 --force-manifest-type <manifest-type> Override auto detection of manifest type 75 --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. 76 -h, --help Usage info 77 " 78 `); 79}); 80 81beforeAll(async () => { 82 // Kill port 83 await execa('kill', ['-9', '$(lsof -ti:19000)']).catch(() => {}); 84}); 85 86for (const args of [ 87 ['--lan', '--tunnel'], 88 ['--offline', '--localhost'], 89 ['--host', 'localhost', '--tunnel'], 90 ['-m', 'localhost', '--lan', '--offline'], 91]) { 92 it(`asserts invalid URL arguments on \`expo start ${args.join(' ')}\``, async () => { 93 await expect(execa('node', [bin, 'start', ...args], { cwd: projectRoot })).rejects.toThrowError( 94 /Specify at most one of/ 95 ); 96 }); 97} 98 99it( 100 'runs `npx expo start`', 101 async () => { 102 const projectRoot = await setupTestProjectAsync('basic-start', 'with-blank'); 103 await fs.remove(path.join(projectRoot, '.expo')); 104 105 const promise = execa('node', [bin, 'start'], { cwd: projectRoot }); 106 107 console.log('Starting server'); 108 109 await new Promise<void>((resolve, reject) => { 110 promise.on('close', (code: number) => { 111 reject( 112 code === 0 113 ? 'Server closed too early. Run `kill -9 $(lsof -ti:19000)` to kill the orphaned process.' 114 : code 115 ); 116 }); 117 118 promise.stdout.on('data', (data) => { 119 const stdout = data.toString(); 120 console.log('output:', stdout); 121 if (stdout.includes('Logs for your project')) { 122 resolve(); 123 } 124 }); 125 }); 126 127 console.log('Fetching manifest'); 128 const results = await fetch('http://localhost:19000/', { 129 headers: { 130 'expo-platform': 'ios', 131 }, 132 }).then((res) => res.json()); 133 134 // Required for Expo Go 135 expect(results.packagerOpts).toStrictEqual({ 136 dev: true, 137 }); 138 expect(results.developer).toStrictEqual({ 139 projectRoot: expect.anything(), 140 tool: 'expo-cli', 141 }); 142 143 // URLs 144 expect(results.bundleUrl).toBe( 145 'http://127.0.0.1:19000/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&hot=false' 146 ); 147 expect(results.debuggerHost).toBe('127.0.0.1:19000'); 148 expect(results.hostUri).toBe('127.0.0.1:19000'); 149 expect(results.logUrl).toBe('http://127.0.0.1:19000/logs'); 150 expect(results.mainModuleName).toBe('node_modules/expo/AppEntry'); 151 152 // Manifest 153 expect(results.sdkVersion).toBe('45.0.0'); 154 expect(results.slug).toBe('basic-start'); 155 expect(results.name).toBe('basic-start'); 156 157 // Custom 158 expect(results.__flipperHack).toBe('React Native packager is running'); 159 160 console.log('Fetching bundle'); 161 const bundle = await fetch(results.bundleUrl).then((res) => res.text()); 162 console.log('Fetched bundle: ', bundle.length); 163 expect(bundle.length).toBeGreaterThan(1000); 164 console.log('Finished'); 165 166 // Kill process. 167 promise.kill('SIGTERM', { 168 forceKillAfterTimeout: 2000, 169 }); 170 171 await promise; 172 }, 173 // Could take 45s depending on how fast npm installs 174 120 * 1000 175); 176