1/* eslint-env jest */
2import fs from 'fs-extra';
3import path from 'path';
4import execa from 'execa';
5import fetch from 'node-fetch';
6import { execute, projectRoot, getLoadedModulesAsync, setupTestProjectAsync, bin } from './utils';
7
8const originalForceColor = process.env.FORCE_COLOR;
9const originalCI = process.env.CI;
10
11beforeAll(async () => {
12  await fs.mkdir(projectRoot, { recursive: true });
13  process.env.FORCE_COLOR = '0';
14  process.env.CI = '1';
15});
16
17afterAll(() => {
18  process.env.FORCE_COLOR = originalForceColor;
19  process.env.CI = originalCI;
20});
21
22it('loads expected modules by default', async () => {
23  const modules = await getLoadedModulesAsync(`require('../../build/src/start').expoStart`);
24  expect(modules).toStrictEqual([
25    '../node_modules/ansi-styles/index.js',
26    '../node_modules/arg/index.js',
27    '../node_modules/chalk/source/index.js',
28    '../node_modules/chalk/source/util.js',
29    '../node_modules/has-flag/index.js',
30    '../node_modules/supports-color/index.js',
31    '@expo/cli/build/src/log.js',
32    '@expo/cli/build/src/start/index.js',
33    '@expo/cli/build/src/utils/args.js',
34    '@expo/cli/build/src/utils/errors.js',
35  ]);
36});
37
38it('runs `npx expo start --help`', async () => {
39  const results = await execute('start', '--help');
40  expect(results.stdout).toMatchInlineSnapshot(`
41    "
42      Description
43        Start a local dev server for the app
44
45      Usage
46        $ npx expo start <dir>
47
48      <dir> is the directory of the Expo project.
49      Defaults to the current working directory.
50
51      Options
52        -a, --android                          Opens your app in Expo Go on a connected Android device
53        -i, --ios                              Opens your app in Expo Go in a currently running iOS simulator on your computer
54        -w, --web                              Opens your app in a web browser
55
56        -c, --clear                            Clear the bundler cache
57        --max-workers <num>                    Maximum number of tasks to allow Metro to spawn
58        --no-dev                               Bundle in production mode
59        --minify                               Minify JavaScript
60
61        -m, --host <mode>                      lan, tunnel, localhost. Dev server hosting type. Default: lan.
62                                               - lan: Use the local network
63                                               - tunnel: Use any network by tunnel through ngrok
64                                               - localhost: Connect to the dev server over localhost
65        --tunnel                               Same as --host tunnel
66        --lan                                  Same as --host lan
67        --localhost                            Same as --host localhost
68
69        --offline                              Skip network requests and use anonymous manifest signatures
70        --https                                Start the dev server with https protocol
71        --scheme <scheme>                      Custom URI protocol to use when launching an app
72        -p, --port <port>                      Port to start the dev server on (does not apply to web or tunnel). Default: 19000
73
74        --dev-client                           Experimental: Starts the bundler for use with the expo-development-client
75        --force-manifest-type <manifest-type>  Override auto detection of manifest type
76        -h, --help                             output usage information
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/').then((res) => res.json());
129
130    // Required for Expo Go
131    expect(results.packagerOpts).toStrictEqual({
132      dev: true,
133    });
134    expect(results.developer).toStrictEqual({
135      projectRoot: expect.anything(),
136      tool: 'expo-cli',
137    });
138
139    // URLs
140    expect(results.bundleUrl).toBe(
141      'http://127.0.0.1:19000/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&hot=false'
142    );
143    expect(results.debuggerHost).toBe('127.0.0.1:19000');
144    expect(results.hostUri).toBe('127.0.0.1:19000');
145    expect(results.logUrl).toBe('http://127.0.0.1:19000/logs');
146    expect(results.mainModuleName).toBe('node_modules/expo/AppEntry');
147
148    // Manifest
149    expect(results.sdkVersion).toBe('44.0.0');
150    expect(results.slug).toBe('basic-start');
151    expect(results.name).toBe('basic-start');
152
153    // Custom
154    expect(results.__flipperHack).toBe('React Native packager is running');
155
156    console.log('Fetching bundle');
157    const bundle = await fetch(results.bundleUrl).then((res) => res.text());
158    console.log('Fetched bundle: ', bundle.length);
159    expect(bundle.length).toBeGreaterThan(1000);
160    console.log('Finished');
161
162    // Kill process.
163    promise.kill('SIGTERM', {
164      forceKillAfterTimeout: 2000,
165    });
166
167    await promise;
168  },
169  // Could take 45s depending on how fast npm installs
170  120 * 1000
171);
172