1import { 2 compileModsAsync, 3 evalModsAsync, 4 ExportedConfig, 5 IOSConfig, 6 withGradleProperties, 7 XML, 8} from '@expo/config-plugins'; 9import JsonFile from '@expo/json-file'; 10import plist from '@expo/plist'; 11import fs from 'fs-extra'; 12import { vol } from 'memfs'; 13import * as path from 'path'; 14import xcode from 'xcode'; 15 16import rnFixture from './fixtures/react-native-project'; 17import { getDirFromFS } from './getDirFromFS'; 18import { 19 withAndroidExpoPlugins, 20 withIosExpoPlugins, 21 withVersionedExpoSDKPlugins, 22} from '../withDefaultPlugins'; 23 24const { withOrientation } = IOSConfig.Orientation; 25 26const { readXMLAsync } = XML; 27const fsReal = jest.requireActual('fs') as typeof fs; 28 29jest.mock('fs'); 30// Weird issues with Android Icon module make it hard to mock test. 31jest.mock('../icons/withAndroidIcons', () => { 32 return { 33 withAndroidIcons(config) { 34 return config; 35 }, 36 setIconAsync() {}, 37 }; 38}); 39const NotificationsPlugin = require('../unversioned/expo-notifications/withAndroidNotifications'); 40NotificationsPlugin.withNotificationIcons = jest.fn((config) => config); 41 42function getLargeConfig(): ExportedConfig { 43 // A very extensive Expo Config. 44 return { 45 name: 'my cool app', 46 slug: 'mycoolapp', 47 description: 'my app is great because it uses expo', 48 // owner?: string; 49 // privacy?: 'public' | 'unlisted' | 'hidden'; 50 // sdkVersion?: string; 51 // runtimeVersion?: string; 52 splash: { 53 backgroundColor: '#ff00ff', 54 }, 55 version: '1.0.0', 56 platforms: ['android', 'ios', 'web'], 57 githubUrl: 'https://github.com/expo/expo', 58 orientation: 'default', 59 userInterfaceStyle: 'dark', 60 backgroundColor: 'orange', 61 primaryColor: '#fff000', 62 // icon: './icons/icon.png', 63 notification: { 64 icon: './icons/notification-icon.png', 65 color: 'green', 66 iosDisplayInForeground: true, 67 androidMode: 'collapse', 68 androidCollapsedTitle: '#{unread_notifications} new interactions', 69 }, 70 androidStatusBar: { 71 barStyle: 'light-content', 72 backgroundColor: '#000FFF', 73 hidden: false, 74 translucent: true, 75 }, 76 androidNavigationBar: { 77 visible: 'sticky-immersive', 78 barStyle: 'dark-content', 79 80 backgroundColor: '#ff0000', 81 }, 82 developmentClient: { 83 silentLaunch: true, 84 }, 85 scheme: 'my-app-redirect', 86 packagerOpts: { 87 extraThing: true, 88 }, 89 updates: { 90 enabled: true, 91 checkAutomatically: 'ON_ERROR_RECOVERY', 92 fallbackToCacheTimeout: 650, 93 }, 94 locales: { 95 en: './locales/en-US.json', 96 es: { foo: 'el bar' }, 97 }, 98 facebookAppId: '1234567890', 99 facebookAutoInitEnabled: true, 100 facebookAutoLogAppEventsEnabled: true, 101 facebookAdvertiserIDCollectionEnabled: true, 102 facebookDisplayName: 'my-fb-test-app', 103 facebookScheme: 'fb1234567890', 104 ios: { 105 bundleIdentifier: 'com.bacon.tester.expoapp', 106 buildNumber: '6.5.0', 107 backgroundColor: '#ff0000', 108 appStoreUrl: 'https://itunes.apple.com/us/app/pillar-valley/id1336398804?ls=1&mt=8', 109 config: { 110 branch: { 111 apiKey: 'MY_BRANCH_KEY', 112 }, 113 usesNonExemptEncryption: true, 114 googleMapsApiKey: 'TEST_googleMapsApiKey', 115 googleMobileAdsAppId: 'TEST_googleMobileAdsAppId', 116 googleMobileAdsAutoInit: true, 117 }, 118 googleServicesFile: './config/GoogleService-Info.plist', 119 supportsTablet: true, 120 isTabletOnly: false, 121 requireFullScreen: true, 122 userInterfaceStyle: 'automatic', 123 infoPlist: { bar: { val: ['foo'] } }, 124 entitlements: { foo: 'bar' }, 125 associatedDomains: ['applinks:https://pillarvalley.netlify.app'], 126 usesIcloudStorage: true, 127 usesAppleSignIn: true, 128 accessesContactNotes: true, 129 }, 130 android: { 131 package: 'com.bacon.tester.expoapp', 132 versionCode: 6, 133 backgroundColor: '#ff0000', 134 userInterfaceStyle: 'light', 135 adaptiveIcon: { 136 foregroundImage: './icons/foreground.png', 137 backgroundImage: './icons/background.png', 138 }, 139 splash: { 140 backgroundColor: '#ff00ff', 141 dark: { 142 backgroundColor: '#00ffff', 143 }, 144 }, 145 blockedPermissions: [ 146 'android.permission.RECORD_AUDIO', 147 'android.permission.ACCESS_FINE_LOCATION', 148 ], 149 permissions: [ 150 'CAMERA', 151 'com.sec.android.provider.badge.permission.WRITE', 152 'android.permission.RECORD_AUDIO', 153 ], 154 googleServicesFile: './config/google-services.json', 155 config: { 156 branch: { 157 apiKey: 'MY_BRANCH_ANDROID_KEY', 158 }, 159 googleMaps: { 160 apiKey: 'MY_GOOGLE_MAPS_ANDROID_KEY', 161 }, 162 googleMobileAdsAppId: 'MY_GOOGLE_MOBILE_ADS_APP_ID', 163 googleMobileAdsAutoInit: true, 164 }, 165 intentFilters: [ 166 { 167 autoVerify: true, 168 action: 'VIEW', 169 data: { 170 scheme: 'https', 171 host: '*.expo.dev', 172 }, 173 category: ['BROWSABLE', 'DEFAULT'], 174 }, 175 ], 176 allowBackup: true, 177 softwareKeyboardLayoutMode: 'pan', 178 }, 179 _internal: { projectRoot: '/app' }, 180 mods: null, 181 }; 182} 183 184function getPrebuildConfig() { 185 let config = { ...getLargeConfig() }; 186 config = withVersionedExpoSDKPlugins(config); 187 188 config = withIosExpoPlugins(config, { 189 bundleIdentifier: 'com.bacon.todo', 190 }); 191 config = withAndroidExpoPlugins(config, { 192 package: 'com.bacon.todo', 193 }); 194 return config; 195} 196describe(evalModsAsync, () => { 197 it(`runs with no core mods`, async () => { 198 let config: ExportedConfig = { 199 name: 'app', 200 slug: '', 201 }; 202 config = await evalModsAsync(config, { projectRoot: '/' }); 203 expect(config.ios).toBeUndefined(); 204 }); 205}); 206 207describe('built-in plugins', () => { 208 const projectRoot = '/app'; 209 const iconPath = path.resolve(__dirname, './fixtures/icon.png'); 210 const icon = fsReal.readFileSync(iconPath) as any; 211 const googleServiceInfoFixture = fsReal.readFileSync( 212 path.resolve(__dirname, './fixtures/GoogleService-Info.plist'), 213 'utf8' 214 ) as any; 215 216 const originalWarn = console.warn; 217 218 beforeEach(async () => { 219 console.warn = jest.fn(); 220 // Trick XDL Info.plist reading 221 Object.defineProperty(process, 'platform', { 222 value: 'not-darwin', 223 }); 224 vol.fromJSON( 225 { 226 // Required to link react-native-maps 227 './node_modules/react-native-maps/package.json': JSON.stringify({}), 228 // App files 229 ...rnFixture, 230 'config/GoogleService-Info.plist': googleServiceInfoFixture, 231 'config/google-services.json': '{}', 232 './icons/foreground.png': icon, 233 './icons/background.png': icon, 234 './icons/notification-icon.png': icon, 235 './icons/ios-icon.png': icon, 236 'locales/en-US.json': JSON.stringify({ foo: 'uhh bar', fallback: 'fallback' }, null, 2), 237 }, 238 projectRoot 239 ); 240 }); 241 242 afterEach(() => { 243 vol.reset(); 244 console.warn = originalWarn; 245 }); 246 247 // Ensure helpful error messages are thrown 248 it(`fails to locate the project name in an invalid project`, async () => { 249 const config = withOrientation({ 250 name: 'app', 251 slug: '', 252 ios: {}, 253 }); 254 await expect(compileModsAsync(config, { projectRoot: '/invalid' })).rejects.toThrow( 255 'Failed to locate Info.plist files relative' 256 ); 257 }); 258 259 it(`skips platforms`, async () => { 260 const config = withOrientation({ 261 name: 'app', 262 slug: '', 263 ios: {}, 264 }); 265 266 // should throw if the platform isn't skipped 267 await compileModsAsync(config, { projectRoot: '/invalid', platforms: ['android'] }); 268 }); 269 270 it('allows conflicts with info.plist overrides', async () => { 271 let config: ExportedConfig = { 272 name: 'app', 273 slug: '', 274 _internal: { projectRoot: '.' }, 275 ios: { 276 config: { 277 usesNonExemptEncryption: false, 278 }, 279 infoPlist: { 280 ITSAppUsesNonExemptEncryption: true, 281 }, 282 }, 283 }; 284 285 config = withIosExpoPlugins(config, { 286 bundleIdentifier: 'com.bacon.todo', 287 }); 288 // Apply mod 289 config = await compileModsAsync(config, { projectRoot: '/app' }); 290 // This should be false because ios.config.usesNonExemptEncryption is used in favor of ios.infoPlist.ITSAppUsesNonExemptEncryption 291 expect(config.ios?.infoPlist?.ITSAppUsesNonExemptEncryption).toBe(true); 292 }); 293 294 it('sends a valid modRequest', async () => { 295 let config = getPrebuildConfig(); 296 297 let modRequest; 298 config = withGradleProperties(config, (config) => { 299 modRequest = config.modRequest; 300 return config; 301 }); 302 // Apply mod 303 await compileModsAsync(config, { introspect: true, projectRoot: '/app' }); 304 305 expect(modRequest).toStrictEqual({ 306 ignoreExistingNativeFiles: false, 307 introspect: true, 308 modName: 'gradleProperties', 309 platform: 'android', 310 platformProjectRoot: '/app/android', 311 projectName: undefined, 312 projectRoot: '/app', 313 }); 314 }); 315 it('compiles mods', async () => { 316 let config = getPrebuildConfig(); 317 // Apply mod 318 config = await compileModsAsync(config, { projectRoot: '/app' }); 319 320 // App config should have been modified 321 expect(config.name).toBe('my cool app'); 322 expect(config.ios?.infoPlist).toBeDefined(); 323 expect(config.ios?.entitlements).toBeDefined(); 324 325 // Google Sign In 326 expect( 327 config.ios?.infoPlist?.CFBundleURLTypes?.find(({ CFBundleURLSchemes }) => 328 CFBundleURLSchemes.includes('com.googleusercontent.apps.1234567890123-abcdef') 329 ) 330 ).toBeDefined(); 331 // Branch 332 expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 333 334 // Mods should all be functions 335 expect(Object.values(config.mods!.ios!).every((value) => typeof value === 'function')).toBe( 336 true 337 ); 338 339 delete config.mods; 340 341 // Shape 342 expect(config).toMatchSnapshot(); 343 344 // Test the written files... 345 const after = getDirFromFS(vol.toJSON(), projectRoot); 346 347 expect(Object.keys(after)).toEqual([ 348 'node_modules/react-native-maps/package.json', 349 'ios/.xcode.env', 350 'ios/HelloWorld/AppDelegate.h', 351 'ios/HelloWorld/AppDelegate.mm', 352 'ios/HelloWorld/Images.xcassets/AppIcon.appiconset/Contents.json', 353 'ios/HelloWorld/Images.xcassets/Contents.json', 354 'ios/HelloWorld/Images.xcassets/SplashScreenBackground.imageset/image.png', 355 'ios/HelloWorld/Images.xcassets/SplashScreenBackground.imageset/Contents.json', 356 'ios/HelloWorld/Info.plist', 357 'ios/HelloWorld/SplashScreen.storyboard', 358 'ios/HelloWorld/Supporting/Expo.plist', 359 'ios/HelloWorld/Supporting/en.lproj/InfoPlist.strings', 360 'ios/HelloWorld/Supporting/es.lproj/InfoPlist.strings', 361 'ios/HelloWorld/main.m', 362 'ios/HelloWorld/GoogleService-Info.plist', 363 'ios/HelloWorld/noop-file.swift', 364 'ios/HelloWorld/HelloWorld-Bridging-Header.h', 365 'ios/HelloWorld/mycoolapp.entitlements', 366 'ios/HelloWorld.xcodeproj/project.pbxproj', 367 'ios/HelloWorld.xcodeproj/project.xcworkspace/contents.xcworkspacedata', 368 'ios/HelloWorld.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 369 'ios/HelloWorld.xcodeproj/xcshareddata/xcschemes/HelloWorld.xcscheme', 370 'ios/HelloWorld.xcworkspace/contents.xcworkspacedata', 371 'ios/HelloWorld.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 372 'ios/Podfile', 373 'ios/Podfile.properties.json', 374 'ios/gitignore', 375 'android/app/build.gradle', 376 'android/app/debug.keystore', 377 'android/app/proguard-rules.pro', 378 'android/app/src/debug/AndroidManifest.xml', 379 'android/app/src/debug/java/com/bacon/todo/ReactNativeFlipper.java', 380 'android/app/src/main/AndroidManifest.xml', 381 'android/app/src/main/java/com/bacon/todo/MainActivity.java', 382 'android/app/src/main/java/com/bacon/todo/MainApplication.java', 383 'android/app/src/main/res/drawable/rn_edit_text_material.xml', 384 'android/app/src/main/res/drawable/splashscreen.xml', 385 'android/app/src/main/res/values/colors.xml', 386 'android/app/src/main/res/values/strings.xml', 387 'android/app/src/main/res/values/styles.xml', 388 'android/app/src/main/res/values-night/colors.xml', 389 'android/app/src/release/java/com/bacon/todo/ReactNativeFlipper.java', 390 'android/app/google-services.json', 391 'android/build.gradle', 392 'android/gitignore', 393 'android/gradle/wrapper/gradle-wrapper.jar', 394 'android/gradle/wrapper/gradle-wrapper.properties', 395 'android/gradle.properties', 396 'android/gradlew', 397 'android/gradlew.bat', 398 'android/settings.gradle', 399 'config/GoogleService-Info.plist', 400 'config/google-services.json', 401 'locales/en-US.json', 402 ]); 403 404 expect(after['ios/HelloWorld/mycoolapp.entitlements']).toMatch( 405 'com.apple.developer.associated-domains' 406 ); 407 408 expect(after['ios/HelloWorld/Info.plist']).toMatch(/com.bacon.todo/); 409 expect(after['ios/HelloWorld/Supporting/en.lproj/InfoPlist.strings']).toMatch( 410 /foo = "uhh bar"/ 411 ); 412 expect(after['ios/HelloWorld/GoogleService-Info.plist']).toBe(googleServiceInfoFixture); 413 414 expect(after['android/app/src/main/java/com/bacon/todo/MainApplication.java']).toMatch( 415 'package com.bacon.todo;' 416 ); 417 418 expect(after['android/app/src/main/res/values/strings.xml']).toMatch( 419 '<string name="app_name">my cool app</string>' 420 ); 421 422 // Ensure files are always written in the correct format 423 for (const xmlPath of [ 424 'android/app/src/main/AndroidManifest.xml', 425 'android/app/src/main/res/values/styles.xml', 426 'android/app/src/main/res/values/strings.xml', 427 'android/app/src/main/res/values/colors.xml', 428 'ios/HelloWorld/Info.plist', 429 ]) { 430 const isValid = await isValidXMLAsync(path.join(projectRoot, xmlPath)); 431 if (!isValid) throw new Error(`Invalid XML file format at: "${xmlPath}"`); 432 } 433 434 // Ensure the infoPlist object is merged correctly 435 const infoPlist = await plist.parse( 436 fs.readFileSync(path.join(projectRoot, 'ios/HelloWorld/Info.plist'), 'utf8') 437 ); 438 expect(infoPlist.bar).toStrictEqual({ val: ['foo'] }); 439 // Ensure the entitlements object is merged correctly 440 const entitlements = await plist.parse( 441 fs.readFileSync(path.join(projectRoot, 'ios/HelloWorld/mycoolapp.entitlements'), 'utf8') 442 ); 443 expect(entitlements.foo).toStrictEqual('bar'); 444 445 // Ensure files are always written in the correct format 446 for (const xmlPath of [ 447 'ios/HelloWorld/Images.xcassets/AppIcon.appiconset/Contents.json', 448 'ios/HelloWorld/Images.xcassets/Contents.json', 449 'android/app/google-services.json', 450 ]) { 451 const isValid = await isValidJSONAsync(path.join(projectRoot, xmlPath)); 452 if (!isValid) throw new Error(`Invalid JSON file format at: "${xmlPath}"`); 453 } 454 455 // Ensure the Xcode project file can be read and parsed. 456 const project = xcode.project( 457 path.join(projectRoot, 'ios/HelloWorld.xcodeproj/project.pbxproj') 458 ); 459 project.parseSync(); 460 }); 461 462 it('introspects mods', async () => { 463 let config = getPrebuildConfig(); 464 465 // Apply mod 466 config = await compileModsAsync(config, { introspect: true, projectRoot: '/app' }); 467 468 // App config should have been modified 469 expect(config.name).toBe('my cool app'); 470 expect(config.ios?.infoPlist).toBeDefined(); 471 expect(config.ios?.entitlements).toBeDefined(); 472 473 // Google Sign In 474 expect( 475 config.ios?.infoPlist?.CFBundleURLTypes?.find(({ CFBundleURLSchemes }) => 476 CFBundleURLSchemes.includes('com.googleusercontent.apps.1234567890123-abcdef') 477 ) 478 ).toBeDefined(); 479 // Branch 480 expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 481 482 const mods = config.mods!; 483 // Mods should all be functions 484 expect(Object.values(mods.ios!).every((value) => typeof value === 'function')).toBe(true); 485 expect(Object.values(mods.android!).every((value) => typeof value === 'function')).toBe(true); 486 // Ensure these mods are removed 487 expect(mods.android?.dangerous).toBeUndefined(); 488 expect(mods.android?.mainActivity).toBeUndefined(); 489 expect(mods.android?.appBuildGradle).toBeUndefined(); 490 expect(mods.android?.projectBuildGradle).toBeUndefined(); 491 expect(mods.android?.settingsGradle).toBeUndefined(); 492 expect(mods.ios?.dangerous).toBeUndefined(); 493 expect(mods.ios?.xcodeproj).toBeUndefined(); 494 495 delete config.mods; 496 497 // Shape 498 expect(config).toMatchSnapshot(); 499 500 expect(config._internal?.modResults).toBeDefined(); 501 expect(config._internal?.modResults.ios.infoPlist).toBeDefined(); 502 expect(config._internal?.modResults.ios.expoPlist).toBeDefined(); 503 expect(config._internal?.modResults.ios.entitlements).toBeDefined(); 504 expect(config._internal?.modResults.android.manifest).toBeDefined(); 505 expect(Array.isArray(config._internal?.modResults.android.gradleProperties)).toBe(true); 506 expect(config._internal?.modResults.android.strings).toBeDefined(); 507 508 // Test the written files... 509 const after = getDirFromFS(vol.toJSON(), projectRoot); 510 511 expect(Object.keys(after)).toEqual([ 512 'node_modules/react-native-maps/package.json', 513 'ios/.xcode.env', 514 'ios/HelloWorld/AppDelegate.h', 515 'ios/HelloWorld/AppDelegate.mm', 516 'ios/HelloWorld/Images.xcassets/AppIcon.appiconset/Contents.json', 517 'ios/HelloWorld/Images.xcassets/Contents.json', 518 'ios/HelloWorld/Images.xcassets/SplashScreen.imageset/Contents.json', 519 'ios/HelloWorld/Images.xcassets/SplashScreenBackground.imageset/Contents.json', 520 'ios/HelloWorld/Info.plist', 521 'ios/HelloWorld/SplashScreen.storyboard', 522 'ios/HelloWorld/Supporting/Expo.plist', 523 'ios/HelloWorld/main.m', 524 'ios/HelloWorld/HelloWorld.entitlements', 525 'ios/HelloWorld.xcodeproj/project.pbxproj', 526 'ios/HelloWorld.xcodeproj/project.xcworkspace/contents.xcworkspacedata', 527 'ios/HelloWorld.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 528 'ios/HelloWorld.xcodeproj/xcshareddata/xcschemes/HelloWorld.xcscheme', 529 'ios/HelloWorld.xcworkspace/contents.xcworkspacedata', 530 'ios/HelloWorld.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 531 'ios/Podfile', 532 'ios/Podfile.properties.json', 533 'ios/gitignore', 534 'android/app/build.gradle', 535 'android/app/debug.keystore', 536 'android/app/proguard-rules.pro', 537 'android/app/src/debug/AndroidManifest.xml', 538 'android/app/src/debug/java/com/helloworld/ReactNativeFlipper.java', 539 'android/app/src/main/AndroidManifest.xml', 540 'android/app/src/main/java/com/helloworld/MainActivity.java', 541 'android/app/src/main/java/com/helloworld/MainApplication.java', 542 'android/app/src/main/res/drawable/rn_edit_text_material.xml', 543 'android/app/src/main/res/drawable/splashscreen.xml', 544 'android/app/src/main/res/values/colors.xml', 545 'android/app/src/main/res/values/strings.xml', 546 'android/app/src/main/res/values/styles.xml', 547 'android/app/src/release/java/com/helloworld/ReactNativeFlipper.java', 548 'android/build.gradle', 549 'android/gitignore', 550 'android/gradle/wrapper/gradle-wrapper.jar', 551 'android/gradle/wrapper/gradle-wrapper.properties', 552 'android/gradle.properties', 553 'android/gradlew', 554 'android/gradlew.bat', 555 'android/settings.gradle', 556 'config/GoogleService-Info.plist', 557 'config/google-services.json', 558 'locales/en-US.json', 559 ]); 560 561 // unmodified 562 expect(after['ios/HelloWorld/HelloWorld.entitlements']).not.toMatch( 563 'com.apple.developer.associated-domains' 564 ); 565 566 expect(after['ios/HelloWorld/Info.plist']).toBe(rnFixture['ios/HelloWorld/Info.plist']); 567 568 expect(after['android/app/src/main/java/com/helloworld/MainApplication.java']).toBe( 569 rnFixture['android/app/src/main/java/com/helloworld/MainApplication.java'] 570 ); 571 expect(after['android/app/src/main/java/com/helloworld/MainActivity.java']).toBe( 572 rnFixture['android/app/src/main/java/com/helloworld/MainActivity.java'] 573 ); 574 expect(after['android/app/src/main/res/values/styles.xml']).toMatch( 575 rnFixture['android/app/src/main/res/values/styles.xml'] 576 ); 577 578 // for (const [name, contents] of Object.entries(rnFixture)) { 579 // // The pbxproj seems to reformat in jest 580 // if (name.includes('pbxproj') || name.endsWith('MainApplicationTurboModuleManagerDelegate.h')) 581 // continue; 582 // expect(after[name]).toMatch(contents); 583 // } 584 // Ensure the Xcode project file can be read and parsed. 585 const project = xcode.project( 586 path.join(projectRoot, 'ios/HelloWorld.xcodeproj/project.pbxproj') 587 ); 588 project.parseSync(); 589 }); 590 591 // Tests that introspection works 592 it('introspects mods in a managed project', async () => { 593 vol.reset(); 594 vol.fromJSON( 595 { 596 // Required to link react-native-maps 597 './node_modules/react-native-maps/package.json': JSON.stringify({}), 598 // App files 599 'config/GoogleService-Info.plist': googleServiceInfoFixture, 600 'config/google-services.json': '{}', 601 'icons/foreground.png': icon, 602 'icons/background.png': icon, 603 'icons/notification-icon.png': icon, 604 'icons/ios-icon.png': icon, 605 'locales/en-US.json': JSON.stringify({ foo: 'uhh bar', fallback: 'fallback' }, null, 2), 606 }, 607 projectRoot 608 ); 609 610 let config = getPrebuildConfig(); 611 612 // Apply mod 613 config = await compileModsAsync(config, { introspect: true, projectRoot: '/app' }); 614 615 // App config should have been modified 616 expect(config.name).toBe('my cool app'); 617 expect(config.ios?.infoPlist).toBeDefined(); 618 expect(config.ios?.entitlements).toBeDefined(); 619 620 // Google Sign In 621 expect( 622 config.ios?.infoPlist?.CFBundleURLTypes?.find(({ CFBundleURLSchemes }) => 623 CFBundleURLSchemes.includes('com.googleusercontent.apps.1234567890123-abcdef') 624 ) 625 ).toBeDefined(); 626 // Branch 627 expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 628 629 const mods = config.mods!; 630 // Mods should all be functions 631 expect(Object.values(mods.ios!).every((value) => typeof value === 'function')).toBe(true); 632 expect(Object.values(mods.android!).every((value) => typeof value === 'function')).toBe(true); 633 // Ensure these mods are removed 634 expect(mods.android?.dangerous).toBeUndefined(); 635 expect(mods.android?.mainActivity).toBeUndefined(); 636 expect(mods.android?.appBuildGradle).toBeUndefined(); 637 expect(mods.android?.projectBuildGradle).toBeUndefined(); 638 expect(mods.android?.settingsGradle).toBeUndefined(); 639 expect(mods.ios?.dangerous).toBeUndefined(); 640 expect(mods.ios?.xcodeproj).toBeUndefined(); 641 642 delete config.mods; 643 644 // Shape 645 expect(config).toMatchSnapshot(); 646 647 expect(config._internal?.modResults).toBeDefined(); 648 expect(config._internal?.modResults.ios.infoPlist).toBeDefined(); 649 expect(config._internal?.modResults.ios.expoPlist).toBeDefined(); 650 expect(config._internal?.modResults.ios.entitlements).toBeDefined(); 651 expect(config._internal?.modResults.android.manifest).toBeDefined(); 652 expect(Array.isArray(config._internal?.modResults.android.gradleProperties)).toBe(true); 653 expect(config._internal?.modResults.android.strings).toBeDefined(); 654 655 // Test the written files... 656 const after = getDirFromFS(vol.toJSON(), projectRoot); 657 658 expect(Object.keys(after)).toStrictEqual([ 659 'node_modules/react-native-maps/package.json', 660 'config/GoogleService-Info.plist', 661 'config/google-services.json', 662 'locales/en-US.json', 663 ]); 664 }); 665 666 it('create Podfile.properties.json file for backward compatible', async () => { 667 const { '/app/ios/Podfile.properties.json': _, ...volWithoutPodfileProperties } = vol.toJSON(); 668 vol.reset(); 669 vol.fromJSON(volWithoutPodfileProperties); 670 671 let config = getPrebuildConfig(); 672 // change jsEngine to hermes 673 config.jsEngine = 'hermes'; 674 675 config = await compileModsAsync(config, { projectRoot: '/app' }); 676 677 const result = await JsonFile.readAsync('/app/ios/Podfile.properties.json'); 678 expect(result).toMatchObject({ 'expo.jsEngine': 'hermes' }); 679 }); 680}); 681 682async function isValidXMLAsync(filePath: string) { 683 try { 684 const res = await readXMLAsync({ path: filePath }); 685 return !!res; 686 } catch { 687 return false; 688 } 689} 690 691async function isValidJSONAsync(filePath: string) { 692 try { 693 const res = await JsonFile.readAsync(filePath); 694 return !!res; 695 } catch { 696 return false; 697 } 698} 699