18d307f52SEvan Bacon/* eslint-env jest */
21ae005aaSWill Schurmanimport {
31ae005aaSWill Schurman  isMultipartPartWithName,
41ae005aaSWill Schurman  parseMultipartMixedResponseAsync,
51ae005aaSWill Schurman} from '@expo/multipart-body-parser';
68d307f52SEvan Baconimport execa from 'execa';
7e377ff85SWill Schurmanimport fs from 'fs-extra';
88d307f52SEvan Baconimport fetch from 'node-fetch';
91ae005aaSWill Schurmanimport nullthrows from 'nullthrows';
10e377ff85SWill Schurmanimport path from 'path';
11e377ff85SWill Schurman
120a6ddb20SEvan Baconimport {
130a6ddb20SEvan Bacon  execute,
140a6ddb20SEvan Bacon  projectRoot,
150a6ddb20SEvan Bacon  getLoadedModulesAsync,
160a6ddb20SEvan Bacon  setupTestProjectAsync,
170a6ddb20SEvan Bacon  bin,
180a6ddb20SEvan Bacon  ensurePortFreeAsync,
190a6ddb20SEvan Bacon} from './utils';
208d307f52SEvan Bacon
218d307f52SEvan Baconconst originalForceColor = process.env.FORCE_COLOR;
228d307f52SEvan Baconconst originalCI = process.env.CI;
238d307f52SEvan Bacon
248d307f52SEvan BaconbeforeAll(async () => {
258d307f52SEvan Bacon  await fs.mkdir(projectRoot, { recursive: true });
268d307f52SEvan Bacon  process.env.FORCE_COLOR = '0';
278d307f52SEvan Bacon  process.env.CI = '1';
288d307f52SEvan Bacon});
298d307f52SEvan Bacon
308d307f52SEvan BaconafterAll(() => {
318d307f52SEvan Bacon  process.env.FORCE_COLOR = originalForceColor;
328d307f52SEvan Bacon  process.env.CI = originalCI;
338d307f52SEvan Bacon});
348d307f52SEvan Bacon
358d307f52SEvan Baconit('loads expected modules by default', async () => {
368d307f52SEvan Bacon  const modules = await getLoadedModulesAsync(`require('../../build/src/start').expoStart`);
378d307f52SEvan Bacon  expect(modules).toStrictEqual([
384067174dSWill Schurman    '../node_modules/ansi-styles/index.js',
398d307f52SEvan Bacon    '../node_modules/arg/index.js',
408d307f52SEvan Bacon    '../node_modules/chalk/source/index.js',
418d307f52SEvan Bacon    '../node_modules/chalk/source/util.js',
428d307f52SEvan Bacon    '../node_modules/has-flag/index.js',
438d307f52SEvan Bacon    '../node_modules/supports-color/index.js',
448d307f52SEvan Bacon    '@expo/cli/build/src/log.js',
458d307f52SEvan Bacon    '@expo/cli/build/src/start/index.js',
468d307f52SEvan Bacon    '@expo/cli/build/src/utils/args.js',
478d307f52SEvan Bacon    '@expo/cli/build/src/utils/errors.js',
488d307f52SEvan Bacon  ]);
498d307f52SEvan Bacon});
508d307f52SEvan Bacon
518d307f52SEvan Baconit('runs `npx expo start --help`', async () => {
528d307f52SEvan Bacon  const results = await execute('start', '--help');
538d307f52SEvan Bacon  expect(results.stdout).toMatchInlineSnapshot(`
548d307f52SEvan Bacon    "
5583d464dcSEvan Bacon      Info
568d307f52SEvan Bacon        Start a local dev server for the app
578d307f52SEvan Bacon
588d307f52SEvan Bacon      Usage
598d307f52SEvan Bacon        $ npx expo start <dir>
608d307f52SEvan Bacon
618d307f52SEvan Bacon      Options
6283d464dcSEvan Bacon        <dir>                           Directory of the Expo project. Default: Current working directory
6341c91838SEvan Bacon        -a, --android                   Open on a connected Android device
6441c91838SEvan Bacon        -i, --ios                       Open in an iOS simulator
6541c91838SEvan Bacon        -w, --web                       Open in a web browser
6641c91838SEvan Bacon
6741c91838SEvan Bacon        -d, --dev-client                Launch in a custom native app
6841c91838SEvan Bacon        -g, --go                        Launch in Expo Go
698d307f52SEvan Bacon
708d307f52SEvan Bacon        -c, --clear                     Clear the bundler cache
7141c91838SEvan Bacon        --max-workers <number>          Maximum number of tasks to allow Metro to spawn
728d307f52SEvan Bacon        --no-dev                        Bundle in production mode
738d307f52SEvan Bacon        --minify                        Minify JavaScript
748d307f52SEvan Bacon
75d7ad395fSEvan Bacon        -m, --host <string>             Dev server hosting type. Default: lan
7683d464dcSEvan Bacon                                        lan: Use the local network
7783d464dcSEvan Bacon                                        tunnel: Use any network by tunnel through ngrok
7883d464dcSEvan Bacon                                        localhost: Connect to the dev server over localhost
798d307f52SEvan Bacon        --tunnel                        Same as --host tunnel
808d307f52SEvan Bacon        --lan                           Same as --host lan
818d307f52SEvan Bacon        --localhost                     Same as --host localhost
828d307f52SEvan Bacon
838d307f52SEvan Bacon        --offline                       Skip network requests and use anonymous manifest signatures
848d307f52SEvan Bacon        --https                         Start the dev server with https protocol
858d307f52SEvan Bacon        --scheme <scheme>               Custom URI protocol to use when launching an app
8647d62600SKudo Chien        -p, --port <number>             Port to start the dev server on (does not apply to web or tunnel). Default: 8081
878d307f52SEvan Bacon
88e1bb5bdfSKudo Chien        --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.
8983d464dcSEvan Bacon        -h, --help                      Usage info
908d307f52SEvan Bacon    "
918d307f52SEvan Bacon  `);
928d307f52SEvan Bacon});
938d307f52SEvan Bacon
948d307f52SEvan Baconfor (const args of [
958d307f52SEvan Bacon  ['--lan', '--tunnel'],
968d307f52SEvan Bacon  ['--offline', '--localhost'],
978d307f52SEvan Bacon  ['--host', 'localhost', '--tunnel'],
988d307f52SEvan Bacon  ['-m', 'localhost', '--lan', '--offline'],
998d307f52SEvan Bacon]) {
1008d307f52SEvan Bacon  it(`asserts invalid URL arguments on \`expo start ${args.join(' ')}\``, async () => {
1018d307f52SEvan Bacon    await expect(execa('node', [bin, 'start', ...args], { cwd: projectRoot })).rejects.toThrowError(
1028d307f52SEvan Bacon      /Specify at most one of/
1038d307f52SEvan Bacon    );
1048d307f52SEvan Bacon  });
1058d307f52SEvan Bacon}
1068d307f52SEvan Bacon
1070a6ddb20SEvan Bacondescribe('server', () => {
1080a6ddb20SEvan Bacon  // Kill port
10947d62600SKudo Chien  const kill = () => ensurePortFreeAsync(8081);
1100a6ddb20SEvan Bacon
1110a6ddb20SEvan Bacon  beforeEach(async () => {
1120a6ddb20SEvan Bacon    await kill();
1130a6ddb20SEvan Bacon  });
1140a6ddb20SEvan Bacon
1150a6ddb20SEvan Bacon  afterAll(async () => {
1160a6ddb20SEvan Bacon    await kill();
1170a6ddb20SEvan Bacon  });
1188d307f52SEvan Bacon  it(
1198d307f52SEvan Bacon    'runs `npx expo start`',
1208d307f52SEvan Bacon    async () => {
1218d307f52SEvan Bacon      const projectRoot = await setupTestProjectAsync('basic-start', 'with-blank');
1228d307f52SEvan Bacon      await fs.remove(path.join(projectRoot, '.expo'));
1238d307f52SEvan Bacon
1248d307f52SEvan Bacon      const promise = execa('node', [bin, 'start'], { cwd: projectRoot });
1258d307f52SEvan Bacon
1268d307f52SEvan Bacon      console.log('Starting server');
1278d307f52SEvan Bacon
1288d307f52SEvan Bacon      await new Promise<void>((resolve, reject) => {
1298d307f52SEvan Bacon        promise.on('close', (code: number) => {
1308d307f52SEvan Bacon          reject(
1318d307f52SEvan Bacon            code === 0
13247d62600SKudo Chien              ? 'Server closed too early. Run `kill -9 $(lsof -ti:8081)` to kill the orphaned process.'
1338d307f52SEvan Bacon              : code
1348d307f52SEvan Bacon          );
1358d307f52SEvan Bacon        });
1368d307f52SEvan Bacon
137e1bb5bdfSKudo Chien        promise.stdout?.on('data', (data) => {
1388d307f52SEvan Bacon          const stdout = data.toString();
1398d307f52SEvan Bacon          console.log('output:', stdout);
1408d307f52SEvan Bacon          if (stdout.includes('Logs for your project')) {
1418d307f52SEvan Bacon            resolve();
1428d307f52SEvan Bacon          }
1438d307f52SEvan Bacon        });
1448d307f52SEvan Bacon      });
1458d307f52SEvan Bacon
1468d307f52SEvan Bacon      console.log('Fetching manifest');
14747d62600SKudo Chien      const response = await fetch('http://localhost:8081/', {
1486d6b81f9SEvan Bacon        headers: {
1496d6b81f9SEvan Bacon          'expo-platform': 'ios',
1501ae005aaSWill Schurman          Accept: 'multipart/mixed',
1516d6b81f9SEvan Bacon        },
1521ae005aaSWill Schurman      });
1531ae005aaSWill Schurman
1541ae005aaSWill Schurman      const multipartParts = await parseMultipartMixedResponseAsync(
1551ae005aaSWill Schurman        response.headers.get('content-type') as string,
1561ae005aaSWill Schurman        await response.buffer()
1571ae005aaSWill Schurman      );
1581ae005aaSWill Schurman      const manifestPart = nullthrows(
1591ae005aaSWill Schurman        multipartParts.find((part) => isMultipartPartWithName(part, 'manifest'))
1601ae005aaSWill Schurman      );
1611ae005aaSWill Schurman
1621ae005aaSWill Schurman      const manifest = JSON.parse(manifestPart.body);
1638d307f52SEvan Bacon
1648d307f52SEvan Bacon      // Required for Expo Go
1651ae005aaSWill Schurman      expect(manifest.extra.expoGo.packagerOpts).toStrictEqual({
1668d307f52SEvan Bacon        dev: true,
1678d307f52SEvan Bacon      });
1681ae005aaSWill Schurman      expect(manifest.extra.expoGo.developer).toStrictEqual({
1698d307f52SEvan Bacon        projectRoot: expect.anything(),
1708d307f52SEvan Bacon        tool: 'expo-cli',
1718d307f52SEvan Bacon      });
1728d307f52SEvan Bacon
1738d307f52SEvan Bacon      // URLs
1741ae005aaSWill Schurman      expect(manifest.launchAsset.url).toBe(
175*465d3694SEvan Bacon        'http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&hot=false'
1768d307f52SEvan Bacon      );
17747d62600SKudo Chien      expect(manifest.extra.expoGo.debuggerHost).toBe('127.0.0.1:8081');
1781ae005aaSWill Schurman      expect(manifest.extra.expoGo.mainModuleName).toBe('node_modules/expo/AppEntry');
17947d62600SKudo Chien      expect(manifest.extra.expoClient.hostUri).toBe('127.0.0.1:8081');
1808d307f52SEvan Bacon
1818d307f52SEvan Bacon      // Manifest
1821ae005aaSWill Schurman      expect(manifest.runtimeVersion).toBe('exposdk:47.0.0');
1831ae005aaSWill Schurman      expect(manifest.extra.expoClient.sdkVersion).toBe('47.0.0');
1841ae005aaSWill Schurman      expect(manifest.extra.expoClient.slug).toBe('basic-start');
1851ae005aaSWill Schurman      expect(manifest.extra.expoClient.name).toBe('basic-start');
1868d307f52SEvan Bacon
1878d307f52SEvan Bacon      // Custom
1881ae005aaSWill Schurman      expect(manifest.extra.expoGo.__flipperHack).toBe('React Native packager is running');
1898d307f52SEvan Bacon
1908d307f52SEvan Bacon      console.log('Fetching bundle');
1911ae005aaSWill Schurman      const bundle = await fetch(manifest.launchAsset.url).then((res) => res.text());
1928d307f52SEvan Bacon      console.log('Fetched bundle: ', bundle.length);
1938d307f52SEvan Bacon      expect(bundle.length).toBeGreaterThan(1000);
1948d307f52SEvan Bacon      console.log('Finished');
1958d307f52SEvan Bacon
1968d307f52SEvan Bacon      // Kill process.
197e1bb5bdfSKudo Chien      promise.kill('SIGTERM');
1988d307f52SEvan Bacon
1998d307f52SEvan Bacon      await promise;
2008d307f52SEvan Bacon    },
2018d307f52SEvan Bacon    // Could take 45s depending on how fast npm installs
2028d307f52SEvan Bacon    120 * 1000
2038d307f52SEvan Bacon  );
2040a6ddb20SEvan Bacon});
205