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