1/* eslint-env jest */ 2import fs from 'fs-extra'; 3import os from 'os'; 4import path from 'path'; 5import stripAnsi from 'strip-ansi'; 6 7import { mockSpawnPromise, mockedSpawnAsync } from '../../__tests__/spawn-utils'; 8import { 9 CocoaPodsPackageManager, 10 getPodRepoUpdateMessage, 11 getPodUpdateMessage, 12} from '../CocoaPodsPackageManager'; 13 14const projectRoot = getTemporaryPath(); 15 16function getTemporaryPath() { 17 return path.join(os.tmpdir(), Math.random().toString(36).substring(2)); 18} 19function getRoot(...args) { 20 return path.join(projectRoot, ...args); 21} 22 23jest.mock('@expo/spawn-async'); 24 25const originalForceColor = process.env.FORCE_COLOR; 26 27beforeEach(() => { 28 process.env.FORCE_COLOR = '1'; 29}); 30 31afterAll(() => { 32 process.env.FORCE_COLOR = originalForceColor; 33}); 34 35const fakePodRepoUpdateErrorOutput = { 36 pid: 74312, 37 // prettier-ignore 38 output: [ 39 'Using Expo modules' + os.EOL + 40 'Auto-linking React Native modules for target `yolo74`: RNGestureHandler, RNReanimated, RNScreens, and react-native-safe-area-context' + os.EOL + 41 'Analyzing dependencies' + os.EOL + 42 '[!] CocoaPods could not find compatible versions for pod "EXFileSystem":' + os.EOL + 43 ' In snapshot (Podfile.lock):' + os.EOL + 44 ' EXFileSystem (from `../node_modules/expo-file-system/ios`)' + os.EOL + 45 '' + os.EOL + 46 ' In Podfile:' + os.EOL + 47 ' EXFileSystem (from `../node_modules/expo-file-system/ios`)' + os.EOL + 48 '' + os.EOL + 49 '' + os.EOL + 50 'You have either:' + os.EOL + 51 ' * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.' + os.EOL + 52 ' * changed the constraints of dependency `EXFileSystem` inside your development pod `EXFileSystem`.' + os.EOL + 53 " You should run `pod update EXFileSystem` to apply changes you've made.\n", 54 'Ignoring ffi-1.13.1 because its extensions are not built. Try: gem pristine ffi --version 1.13.1\n', 55 ], 56 // prettier-ignore 57 stdout: 58 'Using Expo modules' + os.EOL + 59 'Auto-linking React Native modules for target `yolo74`: RNGestureHandler, RNReanimated, RNScreens, and react-native-safe-area-context' + os.EOL + 60 'Analyzing dependencies' + os.EOL + 61 '[!] CocoaPods could not find compatible versions for pod "EXFileSystem":' + os.EOL + 62 ' In snapshot (Podfile.lock):' + os.EOL + 63 ' EXFileSystem (from `../node_modules/expo-file-system/ios`)' + os.EOL + 64 '' + os.EOL + 65 ' In Podfile:' + os.EOL + 66 ' EXFileSystem (from `../node_modules/expo-file-system/ios`)' + os.EOL + 67 '' + os.EOL + 68 '' + os.EOL + 69 'You have either:' + os.EOL + 70 ' * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.' + os.EOL + 71 ' * changed the constraints of dependency `EXFileSystem` inside your development pod `EXFileSystem`.' + os.EOL + 72 " You should run `pod update EXFileSystem` to apply changes you've made.\n", 73 // prettier-ignore 74 stderr: 75 'Ignoring ffi-1.13.1 because its extensions are not built. Try: gem pristine ffi --version 1.13.1' + os.EOL + 76 'Ignoring digest-crc-0.6.1 because its extensions are not built. Try: gem pristine digest-crc --version 0.6.1' + os.EOL + 77 'Ignoring ffi-1.13.1 because its extensions are not built. Try: gem pristine ffi --version 1.13.1' + os.EOL + 78 'Ignoring unf_ext-0.0.7.7 because its extensions are not built. Try: gem pristine unf_ext --version 0.0.7.7\n', 79 status: 1, 80 signal: null, 81}; 82 83describe(getPodUpdateMessage, () => { 84 it(`matches pod update`, () => { 85 expect(getPodUpdateMessage(fakePodRepoUpdateErrorOutput.stdout)).toStrictEqual({ 86 shouldUpdateRepo: true, 87 updatePackage: 'EXFileSystem', 88 }); 89 }); 90 it(`matches pod update without repo update`, () => { 91 expect( 92 getPodUpdateMessage( 93 "It seems like you've changed the version of the dependency `React-Core/RCTWebSocket` and it differs from the version stored in `Pods/Local Podspecs`.\n" + 94 'You should run `pod update React-Core/RCTWebSocket --no-repo-update` to apply changes made locally.\n' 95 ) 96 ).toStrictEqual({ 97 shouldUpdateRepo: false, 98 updatePackage: 'React-Core/RCTWebSocket', 99 }); 100 }); 101}); 102 103describe(getPodRepoUpdateMessage, () => { 104 it(`formats pod repo update message`, () => { 105 expect( 106 stripAnsi( 107 getPodRepoUpdateMessage( 108 '[!] Unable to find a specification for `expo-dev-menu-interface` depended upon by `expo-dev-launcher`' 109 ).message 110 ) 111 ).toBe( 112 `Couldn't install: expo-dev-launcher » expo-dev-menu-interface. Updating the Pods project and trying again...` 113 ); 114 }); 115 it(`formats pod update message`, () => { 116 expect( 117 stripAnsi( 118 getPodRepoUpdateMessage( 119 "You should run `pod update EXFileSystem` to apply changes you've made." 120 ).message 121 ) 122 ).toBe(`Couldn't install: EXFileSystem. Updating the Pods project and trying again...`); 123 }); 124}); 125 126describe('installAsync', () => { 127 it(`does pod repo update automatically when the Podfile.lock is malformed`, async () => { 128 const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager'); 129 const manager = new CocoaPodsPackageManager({ cwd: projectRoot }); 130 131 manager._runAsync = jest.fn((commands: string[]) => { 132 const cmd = commands.join(' '); 133 if (['install', 'update EXFileSystem', 'install --repo-update'].includes(cmd)) { 134 throw fakePodRepoUpdateErrorOutput; 135 } 136 // eslint-disable-next-line no-throw-literal 137 throw 'unhandled ig'; 138 }); 139 140 await expect(manager.installAsync()).rejects.toThrowErrorMatchingInlineSnapshot(` 141 "Command \`pod install --repo-update\` failed. 142 └─ Cause: This is often due to native package versions mismatching. Try deleting the 'ios/Pods' folder or the 'ios/Podfile.lock' file and running 'npx pod-install' to resolve. 143 144 [90m[!] CocoaPods could not find compatible versions for pod "EXFileSystem":[39m 145 [90m In snapshot (Podfile.lock):[39m 146 [90m EXFileSystem (from \`../node_modules/expo-file-system/ios\`)[39m 147 [90m[39m 148 [90m In Podfile:[39m 149 [90m EXFileSystem (from \`../node_modules/expo-file-system/ios\`)[39m 150 [90m[39m 151 [90m[39m 152 [90mYou have either:[39m 153 [90m * out-of-date source repos which you can update with \`pod repo update\` or with \`pod install --repo-update\`.[39m 154 [90m * changed the constraints of dependency \`EXFileSystem\` inside your development pod \`EXFileSystem\`.[39m 155 [90m You should run \`pod update EXFileSystem\` to apply changes you've made.[39m 156 [90m[39m" 157 `); 158 159 // `pod install` > `pod update EXFileSystem` > `pod repo update` > `pod install` 160 expect(manager._runAsync).toHaveBeenNthCalledWith(1, ['install']); 161 expect(manager._runAsync).toHaveBeenNthCalledWith(2, ['update', 'EXFileSystem']); 162 expect(manager._runAsync).toHaveBeenNthCalledWith(3, ['install', '--repo-update']); 163 expect(manager._runAsync).toBeCalledTimes(3); 164 }); 165 166 it(`auto updates malformed package versions`, async () => { 167 const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager'); 168 const manager = new CocoaPodsPackageManager({ cwd: projectRoot }); 169 170 let invokedOnce = false; 171 manager._runAsync = jest.fn((commands: string[]) => { 172 const cmd = commands.join(' '); 173 if (cmd === 'install') { 174 // On the second invocation, return a successful result. 175 if (invokedOnce) { 176 return {}; 177 } 178 invokedOnce = true; 179 throw fakePodRepoUpdateErrorOutput; 180 } 181 if (cmd === 'update EXFileSystem') { 182 return {}; 183 } 184 if (cmd === 'repo update') { 185 return {}; 186 } 187 // eslint-disable-next-line no-throw-literal 188 throw 'unhandled ig'; 189 }); 190 191 // Ensure an error is not thrown 192 await manager.installAsync(); 193 194 // `pod install` > `pod update EXFileSystem` > `pod install` 195 expect(manager._runAsync).toHaveBeenNthCalledWith(1, ['install']); 196 expect(manager._runAsync).toHaveBeenNthCalledWith(2, ['update', 'EXFileSystem']); 197 expect(manager._runAsync).toBeCalledTimes(2); 198 }); 199 200 it(`runs install as expected`, async () => { 201 const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager'); 202 const manager = new CocoaPodsPackageManager({ cwd: projectRoot }); 203 204 manager._runAsync = jest.fn((commands: string[]) => { 205 const cmd = commands.join(' '); 206 if (cmd === 'install') { 207 return {}; 208 } 209 // eslint-disable-next-line no-throw-literal 210 throw 'unhandled ig'; 211 }); 212 213 // Ensure an error is not thrown 214 await manager.installAsync(); 215 216 // `pod install` > success 217 expect(manager._runAsync).toHaveBeenNthCalledWith(1, ['install']); 218 expect(manager._runAsync).toBeCalledTimes(1); 219 }); 220}); 221 222it(`throws for unimplemented methods`, async () => { 223 const manager = new CocoaPodsPackageManager({ cwd: projectRoot }); 224 225 expect(() => manager.addAsync()).toThrow('Unimplemented'); 226 expect(() => manager.addDevAsync()).toThrow('Unimplemented'); 227 expect(() => manager.addGlobalAsync()).toThrow('Unimplemented'); 228 expect(() => manager.removeAsync([])).toThrow('Unimplemented'); 229 expect(() => manager.removeDevAsync([])).toThrow('Unimplemented'); 230 expect(() => manager.removeGlobalAsync([])).toThrow('Unimplemented'); 231 await expect(manager.configAsync('')).rejects.toThrow('Unimplemented'); 232 await expect(manager.removeLockfileAsync()).rejects.toThrow('Unimplemented'); 233 await expect(manager.uninstallAsync()).rejects.toThrow('Unimplemented'); 234}); 235 236it(`gets the cocoapods version`, async () => { 237 const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager'); 238 const manager = new CocoaPodsPackageManager({ cwd: projectRoot }); 239 240 mockedSpawnAsync.mockImplementation(() => mockSpawnPromise(Promise.resolve({ stdout: '1.9.1' }))); 241 242 expect(await manager.versionAsync()).toBe('1.9.1'); 243}); 244 245it(`can detect if the CLI is installed`, async () => { 246 const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager'); 247 const manager = new CocoaPodsPackageManager({ cwd: projectRoot }); 248 249 mockedSpawnAsync.mockImplementation(() => mockSpawnPromise(Promise.resolve({ stdout: '1.9.1' }))); 250 251 expect(await manager.isCLIInstalledAsync()).toBe(true); 252}); 253 254it(`can get the directory of a pods project`, async () => { 255 const projectRoot = getRoot('cocoapods-detect-pods'); 256 const iosRoot = path.join(projectRoot, 'ios'); 257 await fs.ensureDir(iosRoot); 258 259 // first test when no pod project exists 260 const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager'); 261 expect(CocoaPodsPackageManager.getPodProjectRoot(projectRoot)).toBe(null); 262 263 // next test the ios/ folder 264 fs.writeFileSync(path.join(iosRoot, 'Podfile'), '...'); 265 266 expect(CocoaPodsPackageManager.getPodProjectRoot(projectRoot)).toBe(iosRoot); 267 268 // finally test that the current directory has higher priority than the ios directory 269 fs.writeFileSync(path.join(projectRoot, 'Podfile'), '...'); 270 expect(CocoaPodsPackageManager.getPodProjectRoot(projectRoot)).toBe(projectRoot); 271}); 272 273describe('isAvailable', () => { 274 let platform: string; 275 let originalLog: any; 276 beforeAll(() => { 277 platform = process.platform; 278 originalLog = console.log; 279 }); 280 afterEach(() => { 281 Object.defineProperty(process, 'platform', { 282 value: platform, 283 }); 284 console.log = originalLog; 285 }); 286 it(`does not support non-darwin machines`, () => { 287 // set the platform to something other than darwin 288 Object.defineProperty(process, 'platform', { 289 value: 'something-random', 290 }); 291 console.log = jest.fn(); 292 expect(CocoaPodsPackageManager.isAvailable(projectRoot, false)).toBe(false); 293 expect(console.log).toBeCalledTimes(1); 294 }); 295 it(`does not support projects without Podfiles`, async () => { 296 // ensure the platform is darwin 297 Object.defineProperty(process, 'platform', { 298 value: 'darwin', 299 }); 300 // create a fake project without a Podfile 301 const projectRoot = getRoot('cocoapods-detect-available'); 302 await fs.ensureDir(projectRoot); 303 304 let message = ''; 305 console.log = jest.fn((msg) => (message = msg)); 306 307 expect(CocoaPodsPackageManager.isAvailable(projectRoot, false)).toBe(false); 308 expect(console.log).toBeCalledTimes(1); 309 expect(message).toMatch(/not supported in this project/); 310 }); 311}); 312