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      Description
44        Start a local dev server for the app
45
46      Usage
47        $ npx expo start <dir>
48
49      <dir> is the directory of the Expo project.
50      Defaults to the current working directory.
51
52      Options
53        -a, --android                          Opens your app in Expo Go on a connected Android device
54        -i, --ios                              Opens your app in Expo Go in a currently running iOS simulator on your computer
55        -w, --web                              Opens your app in a web browser
56
57        -c, --clear                            Clear the bundler cache
58        --max-workers <num>                    Maximum number of tasks to allow Metro to spawn
59        --no-dev                               Bundle in production mode
60        --minify                               Minify JavaScript
61
62        -m, --host <mode>                      lan, tunnel, localhost. Dev server hosting type. Default: lan.
63                                               - lan: Use the local network
64                                               - tunnel: Use any network by tunnel through ngrok
65                                               - localhost: Connect to the dev server over localhost
66        --tunnel                               Same as --host tunnel
67        --lan                                  Same as --host lan
68        --localhost                            Same as --host localhost
69
70        --offline                              Skip network requests and use anonymous manifest signatures
71        --https                                Start the dev server with https protocol
72        --scheme <scheme>                      Custom URI protocol to use when launching an app
73        -p, --port <port>                      Port to start the dev server on (does not apply to web or tunnel). Default: 19000
74
75        --dev-client                           Experimental: Starts the bundler for use with the expo-development-client
76        --force-manifest-type <manifest-type>  Override auto detection of manifest type
77        --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.
78        -h, --help                             output usage information
79    "
80  `);
81});
82
83beforeAll(async () => {
84  // Kill port
85  await execa('kill', ['-9', '$(lsof -ti:19000)']).catch(() => {});
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
101it(
102  'runs `npx expo start`',
103  async () => {
104    const projectRoot = await setupTestProjectAsync('basic-start', 'with-blank');
105    await fs.remove(path.join(projectRoot, '.expo'));
106
107    const promise = execa('node', [bin, 'start'], { cwd: projectRoot });
108
109    console.log('Starting server');
110
111    await new Promise<void>((resolve, reject) => {
112      promise.on('close', (code: number) => {
113        reject(
114          code === 0
115            ? 'Server closed too early. Run `kill -9 $(lsof -ti:19000)` to kill the orphaned process.'
116            : code
117        );
118      });
119
120      promise.stdout.on('data', (data) => {
121        const stdout = data.toString();
122        console.log('output:', stdout);
123        if (stdout.includes('Logs for your project')) {
124          resolve();
125        }
126      });
127    });
128
129    console.log('Fetching manifest');
130    const results = await fetch('http://localhost:19000/').then((res) => res.json());
131
132    // Required for Expo Go
133    expect(results.packagerOpts).toStrictEqual({
134      dev: true,
135    });
136    expect(results.developer).toStrictEqual({
137      projectRoot: expect.anything(),
138      tool: 'expo-cli',
139    });
140
141    // URLs
142    expect(results.bundleUrl).toBe(
143      'http://127.0.0.1:19000/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&hot=false'
144    );
145    expect(results.debuggerHost).toBe('127.0.0.1:19000');
146    expect(results.hostUri).toBe('127.0.0.1:19000');
147    expect(results.logUrl).toBe('http://127.0.0.1:19000/logs');
148    expect(results.mainModuleName).toBe('node_modules/expo/AppEntry');
149
150    // Manifest
151    expect(results.sdkVersion).toBe('44.0.0');
152    expect(results.slug).toBe('basic-start');
153    expect(results.name).toBe('basic-start');
154
155    // Custom
156    expect(results.__flipperHack).toBe('React Native packager is running');
157
158    console.log('Fetching bundle');
159    const bundle = await fetch(results.bundleUrl).then((res) => res.text());
160    console.log('Fetched bundle: ', bundle.length);
161    expect(bundle.length).toBeGreaterThan(1000);
162    console.log('Finished');
163
164    // Kill process.
165    promise.kill('SIGTERM', {
166      forceKillAfterTimeout: 2000,
167    });
168
169    await promise;
170  },
171  // Could take 45s depending on how fast npm installs
172  120 * 1000
173);
174