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