1import { vol } from 'memfs'; 2 3import { openBrowserAsync } from '../../../utils/open'; 4import { BundlerDevServer, BundlerStartOptions, DevServerInstance } from '../BundlerDevServer'; 5import { UrlCreator } from '../UrlCreator'; 6import { getPlatformBundlers } from '../platformBundlers'; 7 8jest.mock('../../../utils/open'); 9jest.mock(`../../../log`); 10jest.mock('../AsyncNgrok'); 11jest.mock('../DevelopmentSession'); 12jest.mock('../../platforms/ios/ApplePlatformManager', () => { 13 class ApplePlatformManager { 14 openAsync = jest.fn(async () => ({ url: 'mock-apple-url' })); 15 } 16 return { 17 ApplePlatformManager, 18 }; 19}); 20jest.mock('../../platforms/android/AndroidPlatformManager', () => { 21 class AndroidPlatformManager { 22 openAsync = jest.fn(async () => ({ url: 'mock-android-url' })); 23 } 24 return { 25 AndroidPlatformManager, 26 }; 27}); 28 29const originalCwd = process.cwd(); 30 31beforeAll(() => { 32 process.chdir('/'); 33}); 34 35const originalEnv = process.env; 36 37beforeEach(() => { 38 vol.reset(); 39 delete process.env.EXPO_NO_REDIRECT_PAGE; 40}); 41 42afterAll(() => { 43 process.chdir(originalCwd); 44 process.env = originalEnv; 45}); 46 47class MockBundlerDevServer extends BundlerDevServer { 48 get name(): string { 49 return 'fake'; 50 } 51 52 protected async startImplementationAsync( 53 options: BundlerStartOptions 54 ): Promise<DevServerInstance> { 55 const port = options.port || 3000; 56 this.urlCreator = new UrlCreator( 57 { 58 scheme: options.https ? 'https' : 'http', 59 ...options.location, 60 }, 61 { 62 port, 63 getTunnelUrl: this.getTunnelUrl.bind(this), 64 } 65 ); 66 67 const protocol = 'http'; 68 const host = 'localhost'; 69 this.setInstance({ 70 // Server instance 71 server: { close: jest.fn((fn) => fn?.()), addListener() {} }, 72 // URL Info 73 location: { 74 url: `${protocol}://${host}:${port}`, 75 port, 76 protocol, 77 host, 78 }, 79 middleware: {}, 80 // Match the native protocol. 81 messageSocket: { 82 broadcast: jest.fn(), 83 }, 84 }); 85 await this.postStartAsync(options); 86 87 return this.getInstance()!; 88 } 89 90 getPublicUrlCreator() { 91 return this.urlCreator; 92 } 93 getNgrok() { 94 return this.ngrok; 95 } 96 getDevSession() { 97 return this.devSession; 98 } 99 100 protected getConfigModuleIds(): string[] { 101 return ['./fake.config.js']; 102 } 103} 104 105class MockMetroBundlerDevServer extends MockBundlerDevServer { 106 get name(): string { 107 return 'metro'; 108 } 109} 110 111async function getRunningServer() { 112 const devServer = new MockBundlerDevServer('/', getPlatformBundlers({})); 113 await devServer.startAsync({ location: {} }); 114 return devServer; 115} 116 117describe('broadcastMessage', () => { 118 it(`sends a message`, async () => { 119 const devServer = await getRunningServer(); 120 devServer.broadcastMessage('reload', { foo: true }); 121 expect(devServer.getInstance()!.messageSocket.broadcast).toBeCalledWith('reload', { 122 foo: true, 123 }); 124 }); 125}); 126 127describe('openPlatformAsync', () => { 128 it(`opens a project in the browser using tunnel with metro web`, async () => { 129 const devServer = new MockMetroBundlerDevServer('/', getPlatformBundlers({})); 130 await devServer.startAsync({ 131 location: { 132 hostType: 'tunnel', 133 }, 134 }); 135 const { url } = await devServer.openPlatformAsync('desktop'); 136 expect(url).toBe('http://exp.tunnel.dev/foobar'); 137 expect(openBrowserAsync).toBeCalledWith('http://exp.tunnel.dev/foobar'); 138 }); 139 it(`opens a project in the browser`, async () => { 140 const devServer = await getRunningServer(); 141 const { url } = await devServer.openPlatformAsync('desktop'); 142 expect(url).toBe('http://localhost:3000'); 143 expect(openBrowserAsync).toBeCalledWith('http://localhost:3000'); 144 }); 145 146 for (const platform of ['ios', 'android']) { 147 for (const isDevClient of [false, true]) { 148 const runtime = platform === 'ios' ? 'simulator' : 'emulator'; 149 it(`opens an ${platform} project in a ${runtime} (dev client: ${isDevClient})`, async () => { 150 const devServer = await getRunningServer(); 151 devServer.isDevClient = isDevClient; 152 const { url } = await devServer.openPlatformAsync(runtime); 153 154 expect( 155 (await devServer['getPlatformManagerAsync'](runtime)).openAsync 156 ).toHaveBeenNthCalledWith(1, { runtime: isDevClient ? 'custom' : 'expo' }, {}); 157 158 expect(url).toBe(platform === 'ios' ? 'mock-apple-url' : 'mock-android-url'); 159 }); 160 } 161 } 162}); 163 164describe('stopAsync', () => { 165 it(`stops a running dev server`, async () => { 166 const server = new MockBundlerDevServer('/', getPlatformBundlers({})); 167 const instance = await server.startAsync({ 168 location: { 169 hostType: 'tunnel', 170 }, 171 }); 172 const ngrok = server.getNgrok(); 173 const devSession = server.getNgrok(); 174 175 // Ensure services were started. 176 expect(ngrok?.startAsync).toHaveBeenCalled(); 177 expect(devSession?.startAsync).toHaveBeenCalled(); 178 179 // Invoke the stop function 180 await server.stopAsync(); 181 182 // Ensure services were stopped. 183 expect(instance.server.close).toHaveBeenCalled(); 184 expect(ngrok?.stopAsync).toHaveBeenCalled(); 185 expect(devSession?.stopAsync).toHaveBeenCalled(); 186 expect(server.getInstance()).toBeNull(); 187 }); 188}); 189 190describe('isRedirectPageEnabled', () => { 191 beforeEach(() => { 192 vol.reset(); 193 delete process.env.EXPO_NO_REDIRECT_PAGE; 194 }); 195 196 function mockDevClientInstalled() { 197 vol.fromJSON( 198 { 199 'node_modules/expo-dev-client/package.json': '', 200 }, 201 '/' 202 ); 203 } 204 205 it(`is redirect enabled`, async () => { 206 mockDevClientInstalled(); 207 208 const server = new MockBundlerDevServer( 209 '/', 210 getPlatformBundlers({}), 211 // is Dev Client 212 false 213 ); 214 expect(server['isRedirectPageEnabled']()).toBe(true); 215 }); 216 217 it(`redirect can be disabled with env var`, async () => { 218 mockDevClientInstalled(); 219 220 process.env.EXPO_NO_REDIRECT_PAGE = '1'; 221 222 const server = new MockBundlerDevServer( 223 '/', 224 getPlatformBundlers({}), 225 // is Dev Client 226 false 227 ); 228 expect(server['isRedirectPageEnabled']()).toBe(false); 229 }); 230 231 it(`redirect is disabled when running in dev client mode`, async () => { 232 mockDevClientInstalled(); 233 234 const server = new MockBundlerDevServer( 235 '/', 236 getPlatformBundlers({}), 237 // is Dev Client 238 true 239 ); 240 expect(server['isRedirectPageEnabled']()).toBe(false); 241 }); 242 243 it(`redirect is disabled when expo-dev-client is not installed in the project`, async () => { 244 const server = new MockBundlerDevServer( 245 '/', 246 getPlatformBundlers({}), 247 // is Dev Client 248 false 249 ); 250 expect(server['isRedirectPageEnabled']()).toBe(false); 251 }); 252}); 253 254describe('getRedirectUrl', () => { 255 it(`returns null when the redirect page functionality is disabled`, async () => { 256 const server = new MockBundlerDevServer( 257 '/', 258 getPlatformBundlers({}), 259 // is Dev Client 260 false 261 ); 262 server['isRedirectPageEnabled'] = () => false; 263 expect(server['getRedirectUrl']()).toBe(null); 264 }); 265 266 it(`gets the redirect page URL`, async () => { 267 const server = new MockBundlerDevServer('/', getPlatformBundlers({})); 268 server['isRedirectPageEnabled'] = () => true; 269 await server.startAsync({ 270 location: {}, 271 }); 272 273 const urlCreator = server.getPublicUrlCreator()!; 274 urlCreator.constructLoadingUrl = jest.fn(urlCreator.constructLoadingUrl); 275 276 expect(server.getRedirectUrl('emulator')).toBe( 277 'http://100.100.1.100:3000/_expo/loading?platform=android' 278 ); 279 expect(server.getRedirectUrl('simulator')).toBe( 280 'http://100.100.1.100:3000/_expo/loading?platform=ios' 281 ); 282 expect(server.getRedirectUrl(null)).toBe('http://100.100.1.100:3000/_expo/loading'); 283 expect(urlCreator.constructLoadingUrl).toBeCalledTimes(3); 284 }); 285}); 286 287describe('getExpoGoUrl', () => { 288 it(`asserts if the dev server has not been started yet`, () => { 289 const server = new MockBundlerDevServer('/', getPlatformBundlers({})); 290 expect(() => server['getExpoGoUrl']()).toThrow('Dev server instance not found'); 291 }); 292 293 it(`gets the native Expo Go URL`, async () => { 294 const server = new MockBundlerDevServer('/', getPlatformBundlers({})); 295 await server.startAsync({ 296 location: {}, 297 }); 298 299 expect(await server['getExpoGoUrl']()).toBe('exp://100.100.1.100:3000'); 300 }); 301}); 302 303describe('getNativeRuntimeUrl', () => { 304 it(`gets the native runtime URL`, async () => { 305 const server = new MockBundlerDevServer('/', getPlatformBundlers({})); 306 await server.startAsync({ 307 location: {}, 308 }); 309 expect(server.getNativeRuntimeUrl()).toBe('exp://100.100.1.100:3000'); 310 expect(server.getNativeRuntimeUrl({ hostname: 'localhost' })).toBe('exp://127.0.0.1:3000'); 311 expect(server.getNativeRuntimeUrl({ scheme: 'foobar' })).toBe('exp://100.100.1.100:3000'); 312 }); 313 it(`gets the native runtime URL for dev client`, async () => { 314 const server = new MockBundlerDevServer('/', getPlatformBundlers({}), true); 315 await server.startAsync({ 316 location: { 317 scheme: 'my-app', 318 }, 319 }); 320 expect(server.getNativeRuntimeUrl()).toBe( 321 'my-app://expo-development-client/?url=http%3A%2F%2F100.100.1.100%3A3000' 322 ); 323 expect(server.getNativeRuntimeUrl({ hostname: 'localhost' })).toBe( 324 'my-app://expo-development-client/?url=http%3A%2F%2F127.0.0.1%3A3000' 325 ); 326 expect(server.getNativeRuntimeUrl({ scheme: 'foobar' })).toBe( 327 'foobar://expo-development-client/?url=http%3A%2F%2F100.100.1.100%3A3000' 328 ); 329 }); 330}); 331 332describe('getManifestMiddlewareAsync', () => { 333 const server = new MockBundlerDevServer('/', getPlatformBundlers({})); 334 it(`asserts server is not running`, async () => { 335 await expect(server['getManifestMiddlewareAsync']()).rejects.toThrow( 336 /Dev server instance not found/ 337 ); 338 }); 339}); 340 341describe('_startTunnelAsync', () => { 342 const server = new MockBundlerDevServer('/', getPlatformBundlers({})); 343 it(`returns null when the server isn't running`, async () => { 344 expect(await server._startTunnelAsync()).toEqual(null); 345 }); 346}); 347 348describe('getJsInspectorBaseUrl', () => { 349 it('should return http based url', async () => { 350 const devServer = new MockMetroBundlerDevServer('/', getPlatformBundlers({})); 351 await devServer.startAsync({ location: {} }); 352 expect(devServer.getJsInspectorBaseUrl()).toBe('http://100.100.1.100:3000'); 353 }); 354 355 it('should return tunnel url', async () => { 356 const devServer = new MockMetroBundlerDevServer('/', getPlatformBundlers({})); 357 await devServer.startAsync({ location: { hostType: 'tunnel' } }); 358 expect(devServer.getJsInspectorBaseUrl()).toBe('http://exp.tunnel.dev'); 359 }); 360 361 it('should throw error for unsupported bundler', async () => { 362 const devServer = new MockBundlerDevServer('/', getPlatformBundlers({})); 363 await devServer.startAsync({ location: {} }); 364 expect(() => devServer.getJsInspectorBaseUrl()).toThrow(); 365 }); 366}); 367