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