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