1import { asMock } from '../../../__tests__/asMock'; 2import { getVersionsAsync, SDKVersion, Versions } from '../../../api/getVersions'; 3import { downloadExpoGoAsync } from '../../../utils/downloadExpoGoAsync'; 4import { confirmAsync } from '../../../utils/prompts'; 5import { ExpoGoInstaller } from '../ExpoGoInstaller'; 6 7jest.mock('../../../log'); 8jest.mock('../../../utils/prompts'); 9jest.mock('../../../utils/downloadExpoGoAsync'); 10jest.mock('../../../api/getVersions', () => ({ 11 getVersionsAsync: jest.fn(), 12})); 13 14beforeEach(() => { 15 // Reset global memo... 16 ExpoGoInstaller.cache = {}; 17}); 18 19function createInstaller(platform: 'ios' | 'android') { 20 return new ExpoGoInstaller(platform, 'host.fake.expo', '44.0.0'); 21} 22 23function mockVersionsOnce(versions: Partial<Versions>) { 24 asMock(getVersionsAsync).mockResolvedValueOnce(asVersions(versions)); 25} 26 27function asVersions(versions: Partial<Versions>): Versions { 28 return versions as Versions; 29} 30 31describe('_getExpectedClientVersionAsync', () => { 32 it(`returns null when version cannot be found`, async () => { 33 mockVersionsOnce({}); 34 await expect(createInstaller('android')._getExpectedClientVersionAsync()).resolves.toBe(null); 35 }); 36 describe('android', () => { 37 it(`uses platform and most specific version`, async () => { 38 mockVersionsOnce({ 39 androidVersion: '2.0.0', 40 sdkVersions: { 41 '44.0.0': { 42 androidClientVersion: '1.0.0', 43 } as SDKVersion, 44 }, 45 }); 46 await expect(createInstaller('android')._getExpectedClientVersionAsync()).resolves.toBe( 47 '1.0.0' 48 ); 49 }); 50 it(`uses platform and general version`, async () => { 51 mockVersionsOnce({ 52 androidVersion: '2.0.0', 53 sdkVersions: {}, 54 }); 55 await expect(createInstaller('android')._getExpectedClientVersionAsync()).resolves.toBe( 56 '2.0.0' 57 ); 58 }); 59 }); 60 describe('ios', () => { 61 it(`uses platform and most specific version`, async () => { 62 mockVersionsOnce({ 63 iosVersion: '2.0.0', 64 sdkVersions: { 65 '44.0.0': { 66 iosClientVersion: '1.0.0', 67 } as SDKVersion, 68 }, 69 }); 70 71 await expect(createInstaller('ios')._getExpectedClientVersionAsync()).resolves.toBe('1.0.0'); 72 }); 73 it(`uses platform and general version`, async () => { 74 mockVersionsOnce({ 75 iosVersion: '2.0.0', 76 sdkVersions: {}, 77 }); 78 await expect(createInstaller('ios')._getExpectedClientVersionAsync()).resolves.toBe('2.0.0'); 79 }); 80 }); 81}); 82 83describe('isClientOutdatedAsync', () => { 84 it(`returns true if the app is not installed`, async () => { 85 // platform doesn't matter here... 86 const installer = createInstaller('android'); 87 installer._getExpectedClientVersionAsync = jest.fn(); 88 const deviceManager = { 89 getAppVersionAsync: jest.fn(async () => null), 90 } as any; 91 await expect(installer.isClientOutdatedAsync(deviceManager)).resolves.toBe(true); 92 expect(installer._getExpectedClientVersionAsync).toBeCalledTimes(0); 93 }); 94 it(`returns true if the installed Expo Go app is outdated`, async () => { 95 // platform doesn't matter here... 96 const installer = createInstaller('android'); 97 installer._getExpectedClientVersionAsync = jest.fn(async () => '2.0.0'); 98 const deviceManager = { 99 getAppVersionAsync: jest.fn(async () => '1.0.0'), 100 } as any; 101 await expect(installer.isClientOutdatedAsync(deviceManager)).resolves.toBe(true); 102 expect(installer._getExpectedClientVersionAsync).toBeCalledTimes(1); 103 }); 104 it(`returns false if the installed Expo Go app version is up to date`, async () => { 105 // platform doesn't matter here... 106 const installer = createInstaller('android'); 107 installer._getExpectedClientVersionAsync = jest.fn(async () => '2.0.0'); 108 const deviceManager = { 109 getAppVersionAsync: jest.fn(async () => '2.0.0'), 110 } as any; 111 await expect(installer.isClientOutdatedAsync(deviceManager)).resolves.toBe(false); 112 expect(installer._getExpectedClientVersionAsync).toBeCalledTimes(1); 113 }); 114}); 115 116describe('uninstallExpoGoIfOutdatedAsync', () => { 117 function createDeviceManager() { 118 return { 119 name: 'Pixel 3', 120 identifier: '123', 121 uninstallAppAsync: jest.fn(async () => null), 122 } as any; 123 } 124 beforeEach(() => { 125 asMock(confirmAsync).mockReset(); 126 }); 127 it(`returns true when the user uninstalls the outdated app`, async () => { 128 asMock(confirmAsync).mockResolvedValueOnce(true); 129 const installer = createInstaller('android'); 130 installer.isClientOutdatedAsync = jest.fn(async () => true); 131 const deviceManager = createDeviceManager(); 132 133 await expect(installer.uninstallExpoGoIfOutdatedAsync(deviceManager)).resolves.toBe(true); 134 expect(confirmAsync).toBeCalled(); 135 expect(deviceManager.uninstallAppAsync).toBeCalled(); 136 }); 137 138 it(`prevents checking the same device twice`, async () => { 139 asMock(confirmAsync).mockResolvedValueOnce(true); 140 const installer = createInstaller('android'); 141 installer.isClientOutdatedAsync = jest.fn(async () => true); 142 const deviceManager = createDeviceManager(); 143 await expect(installer.uninstallExpoGoIfOutdatedAsync(deviceManager)).resolves.toBe(true); 144 expect(confirmAsync).toBeCalled(); 145 146 // Returns false bacuse the device is already checked. 147 await expect(installer.uninstallExpoGoIfOutdatedAsync(deviceManager)).resolves.toBe(false); 148 expect(confirmAsync).toBeCalledTimes(1); 149 }); 150 151 it(`returns false when the user uninstalls the outdated app`, async () => { 152 asMock(confirmAsync).mockResolvedValueOnce(false); 153 const installer = createInstaller('android'); 154 installer.isClientOutdatedAsync = jest.fn(async () => true); 155 const deviceManager = createDeviceManager(); 156 await expect(installer.uninstallExpoGoIfOutdatedAsync(deviceManager)).resolves.toBe(false); 157 expect(confirmAsync).toBeCalled(); 158 expect(deviceManager.uninstallAppAsync).not.toBeCalled(); 159 }); 160 161 it(`does not actually uninstall the outdated app on iOS`, async () => { 162 asMock(confirmAsync).mockResolvedValueOnce(true); 163 const installer = createInstaller('ios'); 164 installer.isClientOutdatedAsync = jest.fn(async () => true); 165 const deviceManager = createDeviceManager(); 166 await expect(installer.uninstallExpoGoIfOutdatedAsync(deviceManager)).resolves.toBe(true); 167 expect(confirmAsync).toBeCalled(); 168 // Not called because we simply install the new app which automatically overwrites the old one. 169 expect(deviceManager.uninstallAppAsync).not.toBeCalled(); 170 }); 171 it(`does not prompt if the app is not up to date`, async () => { 172 asMock(confirmAsync).mockImplementationOnce(() => { 173 throw new Error('Should not be called'); 174 }); 175 const installer = createInstaller('ios'); 176 installer.isClientOutdatedAsync = jest.fn(async () => false); 177 const deviceManager = createDeviceManager(); 178 await expect(installer.uninstallExpoGoIfOutdatedAsync(deviceManager)).resolves.toBe(false); 179 expect(confirmAsync).not.toBeCalled(); 180 expect(deviceManager.uninstallAppAsync).not.toBeCalled(); 181 }); 182 183 it(`returns false when the project is unversioned`, async () => { 184 const installer = new ExpoGoInstaller('android', 'host.fake.expo', 'UNVERSIONED'); 185 installer.isClientOutdatedAsync = jest.fn(async () => true); 186 const deviceManager = createDeviceManager(); 187 188 await expect(installer.uninstallExpoGoIfOutdatedAsync(deviceManager)).resolves.toBe(false); 189 expect(confirmAsync).not.toBeCalled(); 190 expect(deviceManager.uninstallAppAsync).not.toBeCalled(); 191 }); 192}); 193 194describe('ensureAsync', () => { 195 beforeEach(() => { 196 delete process.env.EXPO_OFFLINE; 197 }); 198 function createDeviceManager(isAppInstalled: boolean) { 199 return { 200 name: 'Pixel 3', 201 identifier: '123', 202 isAppInstalledAsync: jest.fn(async () => isAppInstalled), 203 installAppAsync: jest.fn(async () => {}), 204 } as any; 205 } 206 207 it(`returns true if the app was updated (after being updated)`, async () => { 208 // true because the app is installed. 209 const deviceManager = createDeviceManager(true); 210 211 const installer = createInstaller('android'); 212 // Return true to indicate that the app was uninstalled. 213 installer.uninstallExpoGoIfOutdatedAsync = jest.fn(async () => true); 214 215 await expect(installer.ensureAsync(deviceManager)).resolves.toBe(true); 216 217 expect(downloadExpoGoAsync).toBeCalledTimes(1); 218 expect(deviceManager.installAppAsync).toBeCalled(); 219 }); 220 221 it(`returns true if the app was installed`, async () => { 222 // false because the app is not installed 223 const deviceManager = createDeviceManager(false); 224 225 const installer = createInstaller('android'); 226 227 // App is not installed so we shouldn't check if it's outdated. 228 // This is important because skipping this means we skipped the cache and prompt. 229 installer.uninstallExpoGoIfOutdatedAsync = () => { 230 throw new Error('Should not be called'); 231 }; 232 233 await expect(installer.ensureAsync(deviceManager)).resolves.toBe(true); 234 235 expect(downloadExpoGoAsync).toBeCalledTimes(1); 236 expect(deviceManager.installAppAsync).toBeCalled(); 237 }); 238 239 it(`returns false if the app is up to date`, async () => { 240 // true because the app is up to date. 241 const deviceManager = createDeviceManager(true); 242 243 const installer = createInstaller('android'); 244 245 // Return false to indicate that the app was not uninstalled 246 installer.uninstallExpoGoIfOutdatedAsync = jest.fn(async () => false); 247 248 await expect(installer.ensureAsync(deviceManager)).resolves.toBe(false); 249 250 // No expensive actions. 251 expect(downloadExpoGoAsync).not.toBeCalled(); 252 expect(deviceManager.installAppAsync).not.toBeCalled(); 253 }); 254 255 it(`returns false when installed and running in offline mode`, async () => { 256 process.env.EXPO_OFFLINE = '1'; 257 258 // Reload the Expo Go installer to use the mocked settings 259 const { ExpoGoInstaller } = require('../ExpoGoInstaller'); 260 261 const deviceManager = createDeviceManager(true); 262 const installer = new ExpoGoInstaller('android', 'host.fake.expo', '44.0.0'); 263 264 // Return false and allow the test to validate this is not called 265 installer.uninstallExpoGoIfOutdatedAsync = jest.fn(async () => false); 266 267 await expect(installer.ensureAsync(deviceManager)).resolves.toBe(false); 268 269 // Ensure it avoids making any API requests 270 expect(installer.uninstallExpoGoIfOutdatedAsync).not.toBeCalled(); 271 expect(downloadExpoGoAsync).not.toBeCalled(); 272 }); 273 274 it(`throws when not installed and running in offline mode`, async () => { 275 process.env.EXPO_OFFLINE = '1'; 276 277 // Reload the Expo Go installer to use the mocked settings 278 const { ExpoGoInstaller } = require('../ExpoGoInstaller'); 279 280 const deviceManager = createDeviceManager(false); 281 const installer = new ExpoGoInstaller('android', 'host.fake.expo', '44.0.0'); 282 283 // Return false and allow the test to validate this is not called 284 installer.uninstallExpoGoIfOutdatedAsync = jest.fn(async () => false); 285 286 await expect(installer.ensureAsync(deviceManager)).rejects.toThrowError( 287 'Expo Go is not installed' 288 ); 289 }); 290}); 291