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