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