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