1import openBrowserAsync from 'better-opn';
2import { vol } from 'memfs';
3
4import { BundlerDevServer, BundlerStartOptions, DevServerInstance } from '../BundlerDevServer';
5import { UrlCreator } from '../UrlCreator';
6import { getPlatformBundlers } from '../platformBundlers';
7
8jest.mock('../AsyncNgrok');
9jest.mock('../DevelopmentSession');
10jest.mock('../../platforms/ios/ApplePlatformManager', () => {
11  class ApplePlatformManager {
12    openAsync = jest.fn(async () => ({ url: 'mock-apple-url' }));
13  }
14  return {
15    ApplePlatformManager,
16  };
17});
18jest.mock('../../platforms/android/AndroidPlatformManager', () => {
19  class AndroidPlatformManager {
20    openAsync = jest.fn(async () => ({ url: 'mock-android-url' }));
21  }
22  return {
23    AndroidPlatformManager,
24  };
25});
26
27const originalCwd = process.cwd();
28
29beforeAll(() => {
30  process.chdir('/');
31});
32
33beforeEach(() => {
34  vol.reset();
35  delete process.env.EXPO_ENABLE_INTERSTITIAL_PAGE;
36});
37
38afterAll(() => {
39  process.chdir(originalCwd);
40  delete process.env.EXPO_ENABLE_INTERSTITIAL_PAGE;
41});
42
43class MockBundlerDevServer extends BundlerDevServer {
44  get name(): string {
45    return 'fake';
46  }
47
48  public async startAsync(options: BundlerStartOptions): Promise<DevServerInstance> {
49    const port = options.port || 3000;
50    this.urlCreator = new UrlCreator(
51      {
52        scheme: options.https ? 'https' : 'http',
53        ...options.location,
54      },
55      {
56        port,
57        getTunnelUrl: this.getTunnelUrl.bind(this),
58      }
59    );
60
61    const protocol = 'http';
62    const host = 'localhost';
63    this.setInstance({
64      // Server instance
65      server: { close: jest.fn((fn) => fn()) },
66      // URL Info
67      location: {
68        url: `${protocol}://${host}:${port}`,
69        port,
70        protocol,
71        host,
72      },
73      middleware: {},
74      // Match the native protocol.
75      messageSocket: {
76        broadcast: jest.fn(),
77      },
78    });
79    await this.postStartAsync(options);
80
81    return this.getInstance();
82  }
83
84  getPublicUrlCreator() {
85    return this.urlCreator;
86  }
87  getNgrok() {
88    return this.ngrok;
89  }
90  getDevSession() {
91    return this.devSession;
92  }
93
94  protected getConfigModuleIds(): string[] {
95    return ['./fake.config.js'];
96  }
97
98  public getExpoGoUrl(platform: 'simulator' | 'emulator') {
99    return super.getExpoGoUrl(platform);
100  }
101}
102
103async function getRunningServer() {
104  const devServer = new MockBundlerDevServer('/', getPlatformBundlers({}));
105  await devServer.startAsync({ location: {} });
106  return devServer;
107}
108
109describe('broadcastMessage', () => {
110  it(`sends a message`, async () => {
111    const devServer = await getRunningServer();
112    devServer.broadcastMessage('reload', { foo: true });
113    expect(devServer.getInstance().messageSocket.broadcast).toBeCalledWith('reload', { foo: true });
114  });
115});
116
117describe('openPlatformAsync', () => {
118  it(`opens a project in the browser`, async () => {
119    const devServer = await getRunningServer();
120    const { url } = await devServer.openPlatformAsync('desktop');
121    expect(url).toBe('http://localhost:3000');
122    expect(openBrowserAsync).toBeCalledWith('http://localhost:3000');
123  });
124
125  for (const platform of ['ios', 'android']) {
126    for (const isDevClient of [false, true]) {
127      const runtime = platform === 'ios' ? 'simulator' : 'emulator';
128      it(`opens an ${platform} project in a ${runtime} (dev client: ${isDevClient})`, async () => {
129        const devServer = await getRunningServer();
130        devServer.isDevClient = isDevClient;
131        const { url } = await devServer.openPlatformAsync(runtime);
132
133        expect(
134          (await devServer['getPlatformManagerAsync'](runtime)).openAsync
135        ).toHaveBeenNthCalledWith(1, { runtime: isDevClient ? 'custom' : 'expo' }, {});
136
137        expect(url).toBe(platform === 'ios' ? 'mock-apple-url' : 'mock-android-url');
138      });
139    }
140  }
141});
142
143describe('stopAsync', () => {
144  it(`stops a running dev server`, async () => {
145    const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
146    const instance = await server.startAsync({
147      location: {
148        hostType: 'tunnel',
149      },
150    });
151    const ngrok = server.getNgrok();
152    const devSession = server.getNgrok();
153
154    // Ensure services were started.
155    expect(ngrok.startAsync).toHaveBeenCalled();
156    expect(devSession.startAsync).toHaveBeenCalled();
157
158    // Invoke the stop function
159    await server.stopAsync();
160
161    // Ensure services were stopped.
162    expect(instance.server.close).toHaveBeenCalled();
163    expect(ngrok.stopAsync).toHaveBeenCalled();
164    expect(devSession.stopAsync).toHaveBeenCalled();
165    expect(server.getInstance()).toBeNull();
166  });
167});
168
169describe('getExpoGoUrl', () => {
170  it(`gets the interstitial page URL`, async () => {
171    process.env.EXPO_ENABLE_INTERSTITIAL_PAGE = '1';
172    vol.fromJSON(
173      {
174        'node_modules/expo-dev-launcher/package.json': '',
175      },
176      '/'
177    );
178
179    const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
180    await server.startAsync({
181      location: {},
182    });
183
184    const urlCreator = server.getPublicUrlCreator();
185    urlCreator.constructLoadingUrl = jest.fn(urlCreator.constructLoadingUrl);
186
187    expect(server.getExpoGoUrl('emulator')).toBe(
188      'http://100.100.1.100:3000/_expo/loading?platform=android'
189    );
190    expect(server.getExpoGoUrl('simulator')).toBe(
191      'http://127.0.0.1:3000/_expo/loading?platform=ios'
192    );
193    expect(urlCreator.constructLoadingUrl).toBeCalledTimes(2);
194  });
195  it(`gets the native Expo Go URL`, async () => {
196    const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
197    await server.startAsync({
198      location: {},
199    });
200
201    expect(server.getExpoGoUrl('emulator')).toBe('exp://100.100.1.100:3000');
202    expect(server.getExpoGoUrl('simulator')).toBe('exp://100.100.1.100:3000');
203  });
204});
205
206describe('getNativeRuntimeUrl', () => {
207  it(`gets the native runtime URL`, async () => {
208    const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
209    await server.startAsync({
210      location: {},
211    });
212    expect(server.getNativeRuntimeUrl()).toBe('exp://100.100.1.100:3000');
213    expect(server.getNativeRuntimeUrl({ hostname: 'localhost' })).toBe('exp://127.0.0.1:3000');
214    expect(server.getNativeRuntimeUrl({ scheme: 'foobar' })).toBe('exp://100.100.1.100:3000');
215  });
216  it(`gets the native runtime URL for dev client`, async () => {
217    const server = new MockBundlerDevServer('/', getPlatformBundlers({}), true);
218    await server.startAsync({
219      location: {
220        scheme: 'my-app',
221      },
222    });
223    expect(server.getNativeRuntimeUrl()).toBe(
224      'my-app://expo-development-client/?url=http%3A%2F%2F100.100.1.100%3A3000'
225    );
226    expect(server.getNativeRuntimeUrl({ hostname: 'localhost' })).toBe(
227      'my-app://expo-development-client/?url=http%3A%2F%2F127.0.0.1%3A3000'
228    );
229    expect(server.getNativeRuntimeUrl({ scheme: 'foobar' })).toBe(
230      'foobar://expo-development-client/?url=http%3A%2F%2F100.100.1.100%3A3000'
231    );
232  });
233});
234
235describe('getManifestMiddlewareAsync', () => {
236  const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
237  it(`asserts invalid manifest type`, async () => {
238    await expect(
239      server['getManifestMiddlewareAsync']({
240        // @ts-expect-error
241        forceManifestType: 'foobar',
242      })
243    ).rejects.toThrow(/Manifest middleware for type 'foobar' not found/);
244  });
245  it(`asserts server is not running`, async () => {
246    await expect(server['getManifestMiddlewareAsync']()).rejects.toThrow(
247      /Dev server instance not found/
248    );
249  });
250});
251
252describe('_startTunnelAsync', () => {
253  const server = new MockBundlerDevServer('/', getPlatformBundlers({}));
254  it(`returns null when the server isn't running`, async () => {
255    expect(await server._startTunnelAsync()).toEqual(null);
256  });
257});
258