18d307f52SEvan Baconimport { vol } from 'memfs';
28d307f52SEvan Bacon
3*065a44f7SCedric van Puttenimport { openBrowserAsync } from '../../../utils/open';
48d307f52SEvan Baconimport { BundlerDevServer, BundlerStartOptions, DevServerInstance } from '../BundlerDevServer';
58d307f52SEvan Baconimport { UrlCreator } from '../UrlCreator';
66d6b81f9SEvan Baconimport { getPlatformBundlers } from '../platformBundlers';
78d307f52SEvan Bacon
8*065a44f7SCedric van Puttenjest.mock('../../../utils/open');
9212e3a1aSEric Samelsonjest.mock(`../../../log`);
108d307f52SEvan Baconjest.mock('../AsyncNgrok');
118d307f52SEvan Baconjest.mock('../DevelopmentSession');
128d307f52SEvan Baconjest.mock('../../platforms/ios/ApplePlatformManager', () => {
138d307f52SEvan Bacon  class ApplePlatformManager {
148d307f52SEvan Bacon    openAsync = jest.fn(async () => ({ url: 'mock-apple-url' }));
158d307f52SEvan Bacon  }
168d307f52SEvan Bacon  return {
178d307f52SEvan Bacon    ApplePlatformManager,
188d307f52SEvan Bacon  };
198d307f52SEvan Bacon});
208d307f52SEvan Baconjest.mock('../../platforms/android/AndroidPlatformManager', () => {
218d307f52SEvan Bacon  class AndroidPlatformManager {
228d307f52SEvan Bacon    openAsync = jest.fn(async () => ({ url: 'mock-android-url' }));
238d307f52SEvan Bacon  }
248d307f52SEvan Bacon  return {
258d307f52SEvan Bacon    AndroidPlatformManager,
268d307f52SEvan Bacon  };
278d307f52SEvan Bacon});
288d307f52SEvan Bacon
298d307f52SEvan Baconconst originalCwd = process.cwd();
308d307f52SEvan Bacon
318d307f52SEvan BaconbeforeAll(() => {
328d307f52SEvan Bacon  process.chdir('/');
338d307f52SEvan Bacon});
348d307f52SEvan Bacon
35212e3a1aSEric Samelsonconst originalEnv = process.env;
36212e3a1aSEric Samelson
378d307f52SEvan BaconbeforeEach(() => {
388d307f52SEvan Bacon  vol.reset();
39212e3a1aSEric Samelson  delete process.env.EXPO_NO_REDIRECT_PAGE;
408d307f52SEvan Bacon});
418d307f52SEvan Bacon
428d307f52SEvan BaconafterAll(() => {
438d307f52SEvan Bacon  process.chdir(originalCwd);
44212e3a1aSEric Samelson  process.env = originalEnv;
458d307f52SEvan Bacon});
468d307f52SEvan Bacon
478d307f52SEvan Baconclass MockBundlerDevServer extends BundlerDevServer {
488d307f52SEvan Bacon  get name(): string {
498d307f52SEvan Bacon    return 'fake';
508d307f52SEvan Bacon  }
518d307f52SEvan Bacon
52212e3a1aSEric Samelson  protected async startImplementationAsync(
53212e3a1aSEric Samelson    options: BundlerStartOptions
54212e3a1aSEric Samelson  ): Promise<DevServerInstance> {
558d307f52SEvan Bacon    const port = options.port || 3000;
568d307f52SEvan Bacon    this.urlCreator = new UrlCreator(
578d307f52SEvan Bacon      {
588d307f52SEvan Bacon        scheme: options.https ? 'https' : 'http',
598d307f52SEvan Bacon        ...options.location,
608d307f52SEvan Bacon      },
618d307f52SEvan Bacon      {
628d307f52SEvan Bacon        port,
638d307f52SEvan Bacon        getTunnelUrl: this.getTunnelUrl.bind(this),
648d307f52SEvan Bacon      }
658d307f52SEvan Bacon    );
668d307f52SEvan Bacon
678d307f52SEvan Bacon    const protocol = 'http';
688d307f52SEvan Bacon    const host = 'localhost';
698d307f52SEvan Bacon    this.setInstance({
708d307f52SEvan Bacon      // Server instance
7133643b60SEvan Bacon      server: { close: jest.fn((fn) => fn?.()), addListener() {} },
728d307f52SEvan Bacon      // URL Info
738d307f52SEvan Bacon      location: {
748d307f52SEvan Bacon        url: `${protocol}://${host}:${port}`,
758d307f52SEvan Bacon        port,
768d307f52SEvan Bacon        protocol,
778d307f52SEvan Bacon        host,
788d307f52SEvan Bacon      },
798d307f52SEvan Bacon      middleware: {},
808d307f52SEvan Bacon      // Match the native protocol.
818d307f52SEvan Bacon      messageSocket: {
828d307f52SEvan Bacon        broadcast: jest.fn(),
838d307f52SEvan Bacon      },
848d307f52SEvan Bacon    });
858d307f52SEvan Bacon    await this.postStartAsync(options);
868d307f52SEvan Bacon
87212e3a1aSEric Samelson    return this.getInstance()!;
888d307f52SEvan Bacon  }
898d307f52SEvan Bacon
908d307f52SEvan Bacon  getPublicUrlCreator() {
918d307f52SEvan Bacon    return this.urlCreator;
928d307f52SEvan Bacon  }
938d307f52SEvan Bacon  getNgrok() {
948d307f52SEvan Bacon    return this.ngrok;
958d307f52SEvan Bacon  }
968d307f52SEvan Bacon  getDevSession() {
978d307f52SEvan Bacon    return this.devSession;
988d307f52SEvan Bacon  }
998d307f52SEvan Bacon
1008d307f52SEvan Bacon  protected getConfigModuleIds(): string[] {
1018d307f52SEvan Bacon    return ['./fake.config.js'];
1028d307f52SEvan Bacon  }
1038d307f52SEvan Bacon}
1048d307f52SEvan Bacon
105a91e9b85SEvan Baconclass MockMetroBundlerDevServer extends MockBundlerDevServer {
106a91e9b85SEvan Bacon  get name(): string {
107a91e9b85SEvan Bacon    return 'metro';
108a91e9b85SEvan Bacon  }
109a91e9b85SEvan Bacon}
110a91e9b85SEvan Bacon
1118d307f52SEvan Baconasync function getRunningServer() {
1126d6b81f9SEvan Bacon  const devServer = new MockBundlerDevServer('/', getPlatformBundlers({}));
1138d307f52SEvan Bacon  await devServer.startAsync({ location: {} });
1148d307f52SEvan Bacon  return devServer;
1158d307f52SEvan Bacon}
1168d307f52SEvan Bacon
1178d307f52SEvan Bacondescribe('broadcastMessage', () => {
1188d307f52SEvan Bacon  it(`sends a message`, async () => {
1198d307f52SEvan Bacon    const devServer = await getRunningServer();
1208d307f52SEvan Bacon    devServer.broadcastMessage('reload', { foo: true });
121212e3a1aSEric Samelson    expect(devServer.getInstance()!.messageSocket.broadcast).toBeCalledWith('reload', {
122212e3a1aSEric Samelson      foo: true,
123212e3a1aSEric Samelson    });
1248d307f52SEvan Bacon  });
1258d307f52SEvan Bacon});
1268d307f52SEvan Bacon
1278d307f52SEvan Bacondescribe('openPlatformAsync', () => {
128a91e9b85SEvan Bacon  it(`opens a project in the browser using tunnel with metro web`, async () => {
129a91e9b85SEvan Bacon    const devServer = new MockMetroBundlerDevServer('/', getPlatformBundlers({}));
130a91e9b85SEvan Bacon    await devServer.startAsync({
131a91e9b85SEvan Bacon      location: {
132a91e9b85SEvan Bacon        hostType: 'tunnel',
133a91e9b85SEvan Bacon      },
134a91e9b85SEvan Bacon    });
135a91e9b85SEvan Bacon    const { url } = await devServer.openPlatformAsync('desktop');
136a91e9b85SEvan Bacon    expect(url).toBe('http://exp.tunnel.dev/foobar');
137a91e9b85SEvan Bacon    expect(openBrowserAsync).toBeCalledWith('http://exp.tunnel.dev/foobar');
138a91e9b85SEvan Bacon  });
1398d307f52SEvan Bacon  it(`opens a project in the browser`, async () => {
1408d307f52SEvan Bacon    const devServer = await getRunningServer();
1418d307f52SEvan Bacon    const { url } = await devServer.openPlatformAsync('desktop');
1428d307f52SEvan Bacon    expect(url).toBe('http://localhost:3000');
1438d307f52SEvan Bacon    expect(openBrowserAsync).toBeCalledWith('http://localhost:3000');
1448d307f52SEvan Bacon  });
1458d307f52SEvan Bacon
1468d307f52SEvan Bacon  for (const platform of ['ios', 'android']) {
1478d307f52SEvan Bacon    for (const isDevClient of [false, true]) {
1488d307f52SEvan Bacon      const runtime = platform === 'ios' ? 'simulator' : 'emulator';
1498d307f52SEvan Bacon      it(`opens an ${platform} project in a ${runtime} (dev client: ${isDevClient})`, async () => {
1508d307f52SEvan Bacon        const devServer = await getRunningServer();
1518d307f52SEvan Bacon        devServer.isDevClient = isDevClient;
1528d307f52SEvan Bacon        const { url } = await devServer.openPlatformAsync(runtime);
1538d307f52SEvan Bacon
1548d307f52SEvan Bacon        expect(
1558d307f52SEvan Bacon          (await devServer['getPlatformManagerAsync'](runtime)).openAsync
1568d307f52SEvan Bacon        ).toHaveBeenNthCalledWith(1, { runtime: isDevClient ? 'custom' : 'expo' }, {});
1578d307f52SEvan Bacon
1588d307f52SEvan Bacon        expect(url).toBe(platform === 'ios' ? 'mock-apple-url' : 'mock-android-url');
1598d307f52SEvan Bacon      });
1608d307f52SEvan Bacon    }
1618d307f52SEvan Bacon  }
1628d307f52SEvan Bacon});
1638d307f52SEvan Bacon
1648d307f52SEvan Bacondescribe('stopAsync', () => {
1658d307f52SEvan Bacon  it(`stops a running dev server`, async () => {
1666d6b81f9SEvan Bacon    const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
1678d307f52SEvan Bacon    const instance = await server.startAsync({
1688d307f52SEvan Bacon      location: {
1698d307f52SEvan Bacon        hostType: 'tunnel',
1708d307f52SEvan Bacon      },
1718d307f52SEvan Bacon    });
1728d307f52SEvan Bacon    const ngrok = server.getNgrok();
1738d307f52SEvan Bacon    const devSession = server.getNgrok();
1748d307f52SEvan Bacon
1758d307f52SEvan Bacon    // Ensure services were started.
176212e3a1aSEric Samelson    expect(ngrok?.startAsync).toHaveBeenCalled();
177212e3a1aSEric Samelson    expect(devSession?.startAsync).toHaveBeenCalled();
1788d307f52SEvan Bacon
1798d307f52SEvan Bacon    // Invoke the stop function
1808d307f52SEvan Bacon    await server.stopAsync();
1818d307f52SEvan Bacon
1828d307f52SEvan Bacon    // Ensure services were stopped.
1838d307f52SEvan Bacon    expect(instance.server.close).toHaveBeenCalled();
184212e3a1aSEric Samelson    expect(ngrok?.stopAsync).toHaveBeenCalled();
185212e3a1aSEric Samelson    expect(devSession?.stopAsync).toHaveBeenCalled();
1868d307f52SEvan Bacon    expect(server.getInstance()).toBeNull();
1878d307f52SEvan Bacon  });
1888d307f52SEvan Bacon});
1898d307f52SEvan Bacon
190212e3a1aSEric Samelsondescribe('isRedirectPageEnabled', () => {
191212e3a1aSEric Samelson  beforeEach(() => {
192212e3a1aSEric Samelson    vol.reset();
193212e3a1aSEric Samelson    delete process.env.EXPO_NO_REDIRECT_PAGE;
194212e3a1aSEric Samelson  });
195212e3a1aSEric Samelson
196212e3a1aSEric Samelson  function mockDevClientInstalled() {
1978d307f52SEvan Bacon    vol.fromJSON(
1988d307f52SEvan Bacon      {
199212e3a1aSEric Samelson        'node_modules/expo-dev-client/package.json': '',
2008d307f52SEvan Bacon      },
2018d307f52SEvan Bacon      '/'
2028d307f52SEvan Bacon    );
203212e3a1aSEric Samelson  }
2048d307f52SEvan Bacon
205212e3a1aSEric Samelson  it(`is redirect enabled`, async () => {
206212e3a1aSEric Samelson    mockDevClientInstalled();
207212e3a1aSEric Samelson
208212e3a1aSEric Samelson    const server = new MockBundlerDevServer(
209212e3a1aSEric Samelson      '/',
210212e3a1aSEric Samelson      getPlatformBundlers({}),
211212e3a1aSEric Samelson      // is Dev Client
212212e3a1aSEric Samelson      false
213212e3a1aSEric Samelson    );
214212e3a1aSEric Samelson    expect(server['isRedirectPageEnabled']()).toBe(true);
215212e3a1aSEric Samelson  });
216212e3a1aSEric Samelson
217212e3a1aSEric Samelson  it(`redirect can be disabled with env var`, async () => {
218212e3a1aSEric Samelson    mockDevClientInstalled();
219212e3a1aSEric Samelson
220212e3a1aSEric Samelson    process.env.EXPO_NO_REDIRECT_PAGE = '1';
221212e3a1aSEric Samelson
222212e3a1aSEric Samelson    const server = new MockBundlerDevServer(
223212e3a1aSEric Samelson      '/',
224212e3a1aSEric Samelson      getPlatformBundlers({}),
225212e3a1aSEric Samelson      // is Dev Client
226212e3a1aSEric Samelson      false
227212e3a1aSEric Samelson    );
228212e3a1aSEric Samelson    expect(server['isRedirectPageEnabled']()).toBe(false);
229212e3a1aSEric Samelson  });
230212e3a1aSEric Samelson
231212e3a1aSEric Samelson  it(`redirect is disabled when running in dev client mode`, async () => {
232212e3a1aSEric Samelson    mockDevClientInstalled();
233212e3a1aSEric Samelson
234212e3a1aSEric Samelson    const server = new MockBundlerDevServer(
235212e3a1aSEric Samelson      '/',
236212e3a1aSEric Samelson      getPlatformBundlers({}),
237212e3a1aSEric Samelson      // is Dev Client
238212e3a1aSEric Samelson      true
239212e3a1aSEric Samelson    );
240212e3a1aSEric Samelson    expect(server['isRedirectPageEnabled']()).toBe(false);
241212e3a1aSEric Samelson  });
242212e3a1aSEric Samelson
243212e3a1aSEric Samelson  it(`redirect is disabled when expo-dev-client is not installed in the project`, async () => {
244212e3a1aSEric Samelson    const server = new MockBundlerDevServer(
245212e3a1aSEric Samelson      '/',
246212e3a1aSEric Samelson      getPlatformBundlers({}),
247212e3a1aSEric Samelson      // is Dev Client
248212e3a1aSEric Samelson      false
249212e3a1aSEric Samelson    );
250212e3a1aSEric Samelson    expect(server['isRedirectPageEnabled']()).toBe(false);
251212e3a1aSEric Samelson  });
252212e3a1aSEric Samelson});
253212e3a1aSEric Samelson
254212e3a1aSEric Samelsondescribe('getRedirectUrl', () => {
255212e3a1aSEric Samelson  it(`returns null when the redirect page functionality is disabled`, async () => {
256212e3a1aSEric Samelson    const server = new MockBundlerDevServer(
257212e3a1aSEric Samelson      '/',
258212e3a1aSEric Samelson      getPlatformBundlers({}),
259212e3a1aSEric Samelson      // is Dev Client
260212e3a1aSEric Samelson      false
261212e3a1aSEric Samelson    );
262212e3a1aSEric Samelson    server['isRedirectPageEnabled'] = () => false;
263212e3a1aSEric Samelson    expect(server['getRedirectUrl']()).toBe(null);
264212e3a1aSEric Samelson  });
265212e3a1aSEric Samelson
266212e3a1aSEric Samelson  it(`gets the redirect page URL`, async () => {
2676d6b81f9SEvan Bacon    const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
268212e3a1aSEric Samelson    server['isRedirectPageEnabled'] = () => true;
2698d307f52SEvan Bacon    await server.startAsync({
2708d307f52SEvan Bacon      location: {},
2718d307f52SEvan Bacon    });
2728d307f52SEvan Bacon
273212e3a1aSEric Samelson    const urlCreator = server.getPublicUrlCreator()!;
2748d307f52SEvan Bacon    urlCreator.constructLoadingUrl = jest.fn(urlCreator.constructLoadingUrl);
2758d307f52SEvan Bacon
276212e3a1aSEric Samelson    expect(server.getRedirectUrl('emulator')).toBe(
2778d307f52SEvan Bacon      'http://100.100.1.100:3000/_expo/loading?platform=android'
2788d307f52SEvan Bacon    );
279212e3a1aSEric Samelson    expect(server.getRedirectUrl('simulator')).toBe(
280212e3a1aSEric Samelson      'http://100.100.1.100:3000/_expo/loading?platform=ios'
2818d307f52SEvan Bacon    );
282212e3a1aSEric Samelson    expect(server.getRedirectUrl(null)).toBe('http://100.100.1.100:3000/_expo/loading');
283212e3a1aSEric Samelson    expect(urlCreator.constructLoadingUrl).toBeCalledTimes(3);
2848d307f52SEvan Bacon  });
285212e3a1aSEric Samelson});
286212e3a1aSEric Samelson
287212e3a1aSEric Samelsondescribe('getExpoGoUrl', () => {
288212e3a1aSEric Samelson  it(`asserts if the dev server has not been started yet`, () => {
289212e3a1aSEric Samelson    const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
290212e3a1aSEric Samelson    expect(() => server['getExpoGoUrl']()).toThrow('Dev server instance not found');
291212e3a1aSEric Samelson  });
292212e3a1aSEric Samelson
2938d307f52SEvan Bacon  it(`gets the native Expo Go URL`, async () => {
2946d6b81f9SEvan Bacon    const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
2958d307f52SEvan Bacon    await server.startAsync({
2968d307f52SEvan Bacon      location: {},
2978d307f52SEvan Bacon    });
2988d307f52SEvan Bacon
299212e3a1aSEric Samelson    expect(await server['getExpoGoUrl']()).toBe('exp://100.100.1.100:3000');
3008d307f52SEvan Bacon  });
3018d307f52SEvan Bacon});
3028d307f52SEvan Bacon
3038d307f52SEvan Bacondescribe('getNativeRuntimeUrl', () => {
3048d307f52SEvan Bacon  it(`gets the native runtime URL`, async () => {
3056d6b81f9SEvan Bacon    const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
3068d307f52SEvan Bacon    await server.startAsync({
3078d307f52SEvan Bacon      location: {},
3088d307f52SEvan Bacon    });
3098d307f52SEvan Bacon    expect(server.getNativeRuntimeUrl()).toBe('exp://100.100.1.100:3000');
3108d307f52SEvan Bacon    expect(server.getNativeRuntimeUrl({ hostname: 'localhost' })).toBe('exp://127.0.0.1:3000');
3118d307f52SEvan Bacon    expect(server.getNativeRuntimeUrl({ scheme: 'foobar' })).toBe('exp://100.100.1.100:3000');
3128d307f52SEvan Bacon  });
3138d307f52SEvan Bacon  it(`gets the native runtime URL for dev client`, async () => {
3146d6b81f9SEvan Bacon    const server = new MockBundlerDevServer('/', getPlatformBundlers({}), true);
3158d307f52SEvan Bacon    await server.startAsync({
3168d307f52SEvan Bacon      location: {
3178d307f52SEvan Bacon        scheme: 'my-app',
3188d307f52SEvan Bacon      },
3198d307f52SEvan Bacon    });
3208d307f52SEvan Bacon    expect(server.getNativeRuntimeUrl()).toBe(
3218d307f52SEvan Bacon      'my-app://expo-development-client/?url=http%3A%2F%2F100.100.1.100%3A3000'
3228d307f52SEvan Bacon    );
3238d307f52SEvan Bacon    expect(server.getNativeRuntimeUrl({ hostname: 'localhost' })).toBe(
3248d307f52SEvan Bacon      'my-app://expo-development-client/?url=http%3A%2F%2F127.0.0.1%3A3000'
3258d307f52SEvan Bacon    );
3268d307f52SEvan Bacon    expect(server.getNativeRuntimeUrl({ scheme: 'foobar' })).toBe(
3278d307f52SEvan Bacon      'foobar://expo-development-client/?url=http%3A%2F%2F100.100.1.100%3A3000'
3288d307f52SEvan Bacon    );
3298d307f52SEvan Bacon  });
3308d307f52SEvan Bacon});
3318d307f52SEvan Bacon
3328d307f52SEvan Bacondescribe('getManifestMiddlewareAsync', () => {
3336d6b81f9SEvan Bacon  const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
3348d307f52SEvan Bacon  it(`asserts server is not running`, async () => {
3358d307f52SEvan Bacon    await expect(server['getManifestMiddlewareAsync']()).rejects.toThrow(
3363d6e487dSEvan Bacon      /Dev server instance not found/
3378d307f52SEvan Bacon    );
3388d307f52SEvan Bacon  });
3398d307f52SEvan Bacon});
3408d307f52SEvan Bacon
3418d307f52SEvan Bacondescribe('_startTunnelAsync', () => {
3426d6b81f9SEvan Bacon  const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
3438d307f52SEvan Bacon  it(`returns null when the server isn't running`, async () => {
3448d307f52SEvan Bacon    expect(await server._startTunnelAsync()).toEqual(null);
3458d307f52SEvan Bacon  });
3468d307f52SEvan Bacon});
34757a0d514SKudo Chien
34857a0d514SKudo Chiendescribe('getJsInspectorBaseUrl', () => {
34957a0d514SKudo Chien  it('should return http based url', async () => {
35057a0d514SKudo Chien    const devServer = new MockMetroBundlerDevServer('/', getPlatformBundlers({}));
35157a0d514SKudo Chien    await devServer.startAsync({ location: {} });
35257a0d514SKudo Chien    expect(devServer.getJsInspectorBaseUrl()).toBe('http://100.100.1.100:3000');
35357a0d514SKudo Chien  });
35457a0d514SKudo Chien
35557a0d514SKudo Chien  it('should return tunnel url', async () => {
35657a0d514SKudo Chien    const devServer = new MockMetroBundlerDevServer('/', getPlatformBundlers({}));
35757a0d514SKudo Chien    await devServer.startAsync({ location: { hostType: 'tunnel' } });
358e4018db2Sevanbacon    expect(devServer.getJsInspectorBaseUrl()).toBe('http://exp.tunnel.dev');
35957a0d514SKudo Chien  });
36057a0d514SKudo Chien
36157a0d514SKudo Chien  it('should throw error for unsupported bundler', async () => {
36257a0d514SKudo Chien    const devServer = new MockBundlerDevServer('/', getPlatformBundlers({}));
36357a0d514SKudo Chien    await devServer.startAsync({ location: {} });
36457a0d514SKudo Chien    expect(() => devServer.getJsInspectorBaseUrl()).toThrow();
36557a0d514SKudo Chien  });
36657a0d514SKudo Chien});
367