1082815dcSEvan Baconimport { 2082815dcSEvan Bacon compileModsAsync, 3082815dcSEvan Bacon evalModsAsync, 4082815dcSEvan Bacon ExportedConfig, 5082815dcSEvan Bacon IOSConfig, 6082815dcSEvan Bacon withGradleProperties, 7082815dcSEvan Bacon XML, 8082815dcSEvan Bacon} from '@expo/config-plugins'; 9082815dcSEvan Baconimport JsonFile from '@expo/json-file'; 10082815dcSEvan Baconimport plist from '@expo/plist'; 11082815dcSEvan Baconimport fs from 'fs-extra'; 12082815dcSEvan Baconimport { vol } from 'memfs'; 13082815dcSEvan Baconimport * as path from 'path'; 14082815dcSEvan Baconimport xcode from 'xcode'; 15082815dcSEvan Bacon 168a424bebSJames Ideimport rnFixture from './fixtures/react-native-project'; 178a424bebSJames Ideimport { getDirFromFS } from './getDirFromFS'; 18082815dcSEvan Baconimport { 19082815dcSEvan Bacon withAndroidExpoPlugins, 20082815dcSEvan Bacon withIosExpoPlugins, 21082815dcSEvan Bacon withVersionedExpoSDKPlugins, 22082815dcSEvan Bacon} from '../withDefaultPlugins'; 23082815dcSEvan Bacon 24082815dcSEvan Baconconst { withOrientation } = IOSConfig.Orientation; 25082815dcSEvan Bacon 26082815dcSEvan Baconconst { readXMLAsync } = XML; 27082815dcSEvan Baconconst fsReal = jest.requireActual('fs') as typeof fs; 28082815dcSEvan Bacon 29082815dcSEvan Baconjest.mock('fs'); 30082815dcSEvan Bacon// Weird issues with Android Icon module make it hard to mock test. 31082815dcSEvan Baconjest.mock('../icons/withAndroidIcons', () => { 32082815dcSEvan Bacon return { 33082815dcSEvan Bacon withAndroidIcons(config) { 34082815dcSEvan Bacon return config; 35082815dcSEvan Bacon }, 36082815dcSEvan Bacon setIconAsync() {}, 37082815dcSEvan Bacon }; 38082815dcSEvan Bacon}); 39082815dcSEvan Baconconst NotificationsPlugin = require('../unversioned/expo-notifications/withAndroidNotifications'); 40082815dcSEvan BaconNotificationsPlugin.withNotificationIcons = jest.fn((config) => config); 41082815dcSEvan Bacon 42082815dcSEvan Baconfunction getLargeConfig(): ExportedConfig { 43082815dcSEvan Bacon // A very extensive Expo Config. 44082815dcSEvan Bacon return { 45082815dcSEvan Bacon name: 'my cool app', 46082815dcSEvan Bacon slug: 'mycoolapp', 47082815dcSEvan Bacon description: 'my app is great because it uses expo', 48082815dcSEvan Bacon // owner?: string; 49082815dcSEvan Bacon // privacy?: 'public' | 'unlisted' | 'hidden'; 50082815dcSEvan Bacon // sdkVersion?: string; 51082815dcSEvan Bacon // runtimeVersion?: string; 52082815dcSEvan Bacon splash: { 53082815dcSEvan Bacon backgroundColor: '#ff00ff', 54082815dcSEvan Bacon }, 55082815dcSEvan Bacon version: '1.0.0', 56082815dcSEvan Bacon platforms: ['android', 'ios', 'web'], 57082815dcSEvan Bacon githubUrl: 'https://github.com/expo/expo', 58082815dcSEvan Bacon orientation: 'default', 59082815dcSEvan Bacon userInterfaceStyle: 'dark', 60082815dcSEvan Bacon backgroundColor: 'orange', 61082815dcSEvan Bacon primaryColor: '#fff000', 62082815dcSEvan Bacon // icon: './icons/icon.png', 63082815dcSEvan Bacon notification: { 64082815dcSEvan Bacon icon: './icons/notification-icon.png', 65082815dcSEvan Bacon color: 'green', 66082815dcSEvan Bacon iosDisplayInForeground: true, 67082815dcSEvan Bacon androidMode: 'collapse', 68082815dcSEvan Bacon androidCollapsedTitle: '#{unread_notifications} new interactions', 69082815dcSEvan Bacon }, 70082815dcSEvan Bacon androidStatusBar: { 71082815dcSEvan Bacon barStyle: 'light-content', 72082815dcSEvan Bacon backgroundColor: '#000FFF', 73082815dcSEvan Bacon hidden: false, 74082815dcSEvan Bacon translucent: true, 75082815dcSEvan Bacon }, 76082815dcSEvan Bacon androidNavigationBar: { 77082815dcSEvan Bacon visible: 'sticky-immersive', 78082815dcSEvan Bacon barStyle: 'dark-content', 79082815dcSEvan Bacon 80082815dcSEvan Bacon backgroundColor: '#ff0000', 81082815dcSEvan Bacon }, 82082815dcSEvan Bacon developmentClient: { 83082815dcSEvan Bacon silentLaunch: true, 84082815dcSEvan Bacon }, 85082815dcSEvan Bacon scheme: 'my-app-redirect', 86082815dcSEvan Bacon packagerOpts: { 87082815dcSEvan Bacon extraThing: true, 88082815dcSEvan Bacon }, 89082815dcSEvan Bacon updates: { 90082815dcSEvan Bacon enabled: true, 91082815dcSEvan Bacon checkAutomatically: 'ON_ERROR_RECOVERY', 92082815dcSEvan Bacon fallbackToCacheTimeout: 650, 93082815dcSEvan Bacon }, 94082815dcSEvan Bacon locales: { 95082815dcSEvan Bacon en: './locales/en-US.json', 96082815dcSEvan Bacon es: { foo: 'el bar' }, 97082815dcSEvan Bacon }, 98082815dcSEvan Bacon facebookAppId: '1234567890', 99082815dcSEvan Bacon facebookAutoInitEnabled: true, 100082815dcSEvan Bacon facebookAutoLogAppEventsEnabled: true, 101082815dcSEvan Bacon facebookAdvertiserIDCollectionEnabled: true, 102082815dcSEvan Bacon facebookDisplayName: 'my-fb-test-app', 103082815dcSEvan Bacon facebookScheme: 'fb1234567890', 104082815dcSEvan Bacon ios: { 105082815dcSEvan Bacon bundleIdentifier: 'com.bacon.tester.expoapp', 106082815dcSEvan Bacon buildNumber: '6.5.0', 107082815dcSEvan Bacon backgroundColor: '#ff0000', 108082815dcSEvan Bacon appStoreUrl: 'https://itunes.apple.com/us/app/pillar-valley/id1336398804?ls=1&mt=8', 109082815dcSEvan Bacon config: { 110082815dcSEvan Bacon branch: { 111082815dcSEvan Bacon apiKey: 'MY_BRANCH_KEY', 112082815dcSEvan Bacon }, 113082815dcSEvan Bacon usesNonExemptEncryption: true, 114082815dcSEvan Bacon googleMapsApiKey: 'TEST_googleMapsApiKey', 115082815dcSEvan Bacon googleMobileAdsAppId: 'TEST_googleMobileAdsAppId', 116082815dcSEvan Bacon googleMobileAdsAutoInit: true, 117082815dcSEvan Bacon }, 118082815dcSEvan Bacon googleServicesFile: './config/GoogleService-Info.plist', 119082815dcSEvan Bacon supportsTablet: true, 120082815dcSEvan Bacon isTabletOnly: false, 121082815dcSEvan Bacon requireFullScreen: true, 122082815dcSEvan Bacon userInterfaceStyle: 'automatic', 123082815dcSEvan Bacon infoPlist: { bar: { val: ['foo'] } }, 124082815dcSEvan Bacon entitlements: { foo: 'bar' }, 125082815dcSEvan Bacon associatedDomains: ['applinks:https://pillarvalley.netlify.app'], 126082815dcSEvan Bacon usesIcloudStorage: true, 127082815dcSEvan Bacon usesAppleSignIn: true, 128082815dcSEvan Bacon accessesContactNotes: true, 129082815dcSEvan Bacon }, 130082815dcSEvan Bacon android: { 131082815dcSEvan Bacon package: 'com.bacon.tester.expoapp', 132082815dcSEvan Bacon versionCode: 6, 133082815dcSEvan Bacon backgroundColor: '#ff0000', 134082815dcSEvan Bacon userInterfaceStyle: 'light', 135082815dcSEvan Bacon adaptiveIcon: { 136082815dcSEvan Bacon foregroundImage: './icons/foreground.png', 137082815dcSEvan Bacon backgroundImage: './icons/background.png', 138082815dcSEvan Bacon }, 139082815dcSEvan Bacon splash: { 140082815dcSEvan Bacon backgroundColor: '#ff00ff', 141082815dcSEvan Bacon dark: { 142082815dcSEvan Bacon backgroundColor: '#00ffff', 143082815dcSEvan Bacon }, 144082815dcSEvan Bacon }, 145082815dcSEvan Bacon blockedPermissions: [ 146082815dcSEvan Bacon 'android.permission.RECORD_AUDIO', 147082815dcSEvan Bacon 'android.permission.ACCESS_FINE_LOCATION', 148082815dcSEvan Bacon ], 149082815dcSEvan Bacon permissions: [ 150082815dcSEvan Bacon 'CAMERA', 151082815dcSEvan Bacon 'com.sec.android.provider.badge.permission.WRITE', 152082815dcSEvan Bacon 'android.permission.RECORD_AUDIO', 153082815dcSEvan Bacon ], 154082815dcSEvan Bacon googleServicesFile: './config/google-services.json', 155082815dcSEvan Bacon config: { 156082815dcSEvan Bacon branch: { 157082815dcSEvan Bacon apiKey: 'MY_BRANCH_ANDROID_KEY', 158082815dcSEvan Bacon }, 159082815dcSEvan Bacon googleMaps: { 160082815dcSEvan Bacon apiKey: 'MY_GOOGLE_MAPS_ANDROID_KEY', 161082815dcSEvan Bacon }, 162082815dcSEvan Bacon googleMobileAdsAppId: 'MY_GOOGLE_MOBILE_ADS_APP_ID', 163082815dcSEvan Bacon googleMobileAdsAutoInit: true, 164082815dcSEvan Bacon }, 165082815dcSEvan Bacon intentFilters: [ 166082815dcSEvan Bacon { 167082815dcSEvan Bacon autoVerify: true, 168082815dcSEvan Bacon action: 'VIEW', 169082815dcSEvan Bacon data: { 170082815dcSEvan Bacon scheme: 'https', 171082815dcSEvan Bacon host: '*.expo.dev', 172082815dcSEvan Bacon }, 173082815dcSEvan Bacon category: ['BROWSABLE', 'DEFAULT'], 174082815dcSEvan Bacon }, 175082815dcSEvan Bacon ], 176082815dcSEvan Bacon allowBackup: true, 177082815dcSEvan Bacon softwareKeyboardLayoutMode: 'pan', 178082815dcSEvan Bacon }, 179082815dcSEvan Bacon _internal: { projectRoot: '/app' }, 180082815dcSEvan Bacon mods: null, 181082815dcSEvan Bacon }; 182082815dcSEvan Bacon} 183082815dcSEvan Bacon 184082815dcSEvan Baconfunction getPrebuildConfig() { 185082815dcSEvan Bacon let config = { ...getLargeConfig() }; 186*82ade864SWill Schurman config = withVersionedExpoSDKPlugins(config); 187082815dcSEvan Bacon 188082815dcSEvan Bacon config = withIosExpoPlugins(config, { 189082815dcSEvan Bacon bundleIdentifier: 'com.bacon.todo', 190082815dcSEvan Bacon }); 191082815dcSEvan Bacon config = withAndroidExpoPlugins(config, { 192082815dcSEvan Bacon package: 'com.bacon.todo', 193082815dcSEvan Bacon }); 194082815dcSEvan Bacon return config; 195082815dcSEvan Bacon} 196082815dcSEvan Bacondescribe(evalModsAsync, () => { 197082815dcSEvan Bacon it(`runs with no core mods`, async () => { 198082815dcSEvan Bacon let config: ExportedConfig = { 199082815dcSEvan Bacon name: 'app', 200082815dcSEvan Bacon slug: '', 201082815dcSEvan Bacon }; 202082815dcSEvan Bacon config = await evalModsAsync(config, { projectRoot: '/' }); 203082815dcSEvan Bacon expect(config.ios).toBeUndefined(); 204082815dcSEvan Bacon }); 205082815dcSEvan Bacon}); 206082815dcSEvan Bacon 207082815dcSEvan Bacondescribe('built-in plugins', () => { 208082815dcSEvan Bacon const projectRoot = '/app'; 209082815dcSEvan Bacon const iconPath = path.resolve(__dirname, './fixtures/icon.png'); 210082815dcSEvan Bacon const icon = fsReal.readFileSync(iconPath) as any; 2110060b409SEvan Bacon const googleServiceInfoFixture = fsReal.readFileSync( 2120060b409SEvan Bacon path.resolve(__dirname, './fixtures/GoogleService-Info.plist'), 2130060b409SEvan Bacon 'utf8' 2140060b409SEvan Bacon ) as any; 215082815dcSEvan Bacon 216ed3bd27bSEvan Bacon const originalWarn = console.warn; 217ed3bd27bSEvan Bacon 218082815dcSEvan Bacon beforeEach(async () => { 219ed3bd27bSEvan Bacon console.warn = jest.fn(); 220082815dcSEvan Bacon // Trick XDL Info.plist reading 221082815dcSEvan Bacon Object.defineProperty(process, 'platform', { 222082815dcSEvan Bacon value: 'not-darwin', 223082815dcSEvan Bacon }); 224082815dcSEvan Bacon vol.fromJSON( 225082815dcSEvan Bacon { 226082815dcSEvan Bacon // Required to link react-native-maps 227082815dcSEvan Bacon './node_modules/react-native-maps/package.json': JSON.stringify({}), 228082815dcSEvan Bacon // App files 229082815dcSEvan Bacon ...rnFixture, 2300060b409SEvan Bacon 'config/GoogleService-Info.plist': googleServiceInfoFixture, 231082815dcSEvan Bacon 'config/google-services.json': '{}', 232082815dcSEvan Bacon './icons/foreground.png': icon, 233082815dcSEvan Bacon './icons/background.png': icon, 234082815dcSEvan Bacon './icons/notification-icon.png': icon, 235082815dcSEvan Bacon './icons/ios-icon.png': icon, 236082815dcSEvan Bacon 'locales/en-US.json': JSON.stringify({ foo: 'uhh bar', fallback: 'fallback' }, null, 2), 237082815dcSEvan Bacon }, 238082815dcSEvan Bacon projectRoot 239082815dcSEvan Bacon ); 240082815dcSEvan Bacon }); 241082815dcSEvan Bacon 242082815dcSEvan Bacon afterEach(() => { 243082815dcSEvan Bacon vol.reset(); 244ed3bd27bSEvan Bacon console.warn = originalWarn; 245082815dcSEvan Bacon }); 246082815dcSEvan Bacon 247082815dcSEvan Bacon // Ensure helpful error messages are thrown 248082815dcSEvan Bacon it(`fails to locate the project name in an invalid project`, async () => { 249082815dcSEvan Bacon const config = withOrientation({ 250082815dcSEvan Bacon name: 'app', 251082815dcSEvan Bacon slug: '', 252082815dcSEvan Bacon ios: {}, 253082815dcSEvan Bacon }); 254082815dcSEvan Bacon await expect(compileModsAsync(config, { projectRoot: '/invalid' })).rejects.toThrow( 255082815dcSEvan Bacon 'Failed to locate Info.plist files relative' 256082815dcSEvan Bacon ); 257082815dcSEvan Bacon }); 258082815dcSEvan Bacon 259082815dcSEvan Bacon it(`skips platforms`, async () => { 260082815dcSEvan Bacon const config = withOrientation({ 261082815dcSEvan Bacon name: 'app', 262082815dcSEvan Bacon slug: '', 263082815dcSEvan Bacon ios: {}, 264082815dcSEvan Bacon }); 265082815dcSEvan Bacon 266082815dcSEvan Bacon // should throw if the platform isn't skipped 267082815dcSEvan Bacon await compileModsAsync(config, { projectRoot: '/invalid', platforms: ['android'] }); 268082815dcSEvan Bacon }); 269082815dcSEvan Bacon 270082815dcSEvan Bacon it('allows conflicts with info.plist overrides', async () => { 271082815dcSEvan Bacon let config: ExportedConfig = { 272082815dcSEvan Bacon name: 'app', 273082815dcSEvan Bacon slug: '', 274082815dcSEvan Bacon _internal: { projectRoot: '.' }, 275082815dcSEvan Bacon ios: { 276082815dcSEvan Bacon config: { 277082815dcSEvan Bacon usesNonExemptEncryption: false, 278082815dcSEvan Bacon }, 279082815dcSEvan Bacon infoPlist: { 280082815dcSEvan Bacon ITSAppUsesNonExemptEncryption: true, 281082815dcSEvan Bacon }, 282082815dcSEvan Bacon }, 283082815dcSEvan Bacon }; 284082815dcSEvan Bacon 285082815dcSEvan Bacon config = withIosExpoPlugins(config, { 286082815dcSEvan Bacon bundleIdentifier: 'com.bacon.todo', 287082815dcSEvan Bacon }); 288082815dcSEvan Bacon // Apply mod 289082815dcSEvan Bacon config = await compileModsAsync(config, { projectRoot: '/app' }); 290082815dcSEvan Bacon // This should be false because ios.config.usesNonExemptEncryption is used in favor of ios.infoPlist.ITSAppUsesNonExemptEncryption 291082815dcSEvan Bacon expect(config.ios?.infoPlist?.ITSAppUsesNonExemptEncryption).toBe(true); 292082815dcSEvan Bacon }); 293082815dcSEvan Bacon 294082815dcSEvan Bacon it('sends a valid modRequest', async () => { 295082815dcSEvan Bacon let config = getPrebuildConfig(); 296082815dcSEvan Bacon 297082815dcSEvan Bacon let modRequest; 298082815dcSEvan Bacon config = withGradleProperties(config, (config) => { 299082815dcSEvan Bacon modRequest = config.modRequest; 300082815dcSEvan Bacon return config; 301082815dcSEvan Bacon }); 302082815dcSEvan Bacon // Apply mod 303082815dcSEvan Bacon await compileModsAsync(config, { introspect: true, projectRoot: '/app' }); 304082815dcSEvan Bacon 305082815dcSEvan Bacon expect(modRequest).toStrictEqual({ 306e501a7c6SGabriel Donadel Dall'Agnol ignoreExistingNativeFiles: false, 307082815dcSEvan Bacon introspect: true, 308082815dcSEvan Bacon modName: 'gradleProperties', 309082815dcSEvan Bacon platform: 'android', 310082815dcSEvan Bacon platformProjectRoot: '/app/android', 311082815dcSEvan Bacon projectName: undefined, 312082815dcSEvan Bacon projectRoot: '/app', 313082815dcSEvan Bacon }); 314082815dcSEvan Bacon }); 315082815dcSEvan Bacon it('compiles mods', async () => { 316082815dcSEvan Bacon let config = getPrebuildConfig(); 317082815dcSEvan Bacon // Apply mod 318082815dcSEvan Bacon config = await compileModsAsync(config, { projectRoot: '/app' }); 319082815dcSEvan Bacon 320082815dcSEvan Bacon // App config should have been modified 321082815dcSEvan Bacon expect(config.name).toBe('my cool app'); 3225f51f606SEvan Bacon expect(config.ios?.infoPlist).toBeDefined(); 3235f51f606SEvan Bacon expect(config.ios?.entitlements).toBeDefined(); 324082815dcSEvan Bacon 325082815dcSEvan Bacon // Google Sign In 326082815dcSEvan Bacon expect( 327082815dcSEvan Bacon config.ios?.infoPlist?.CFBundleURLTypes?.find(({ CFBundleURLSchemes }) => 3280060b409SEvan Bacon CFBundleURLSchemes.includes('com.googleusercontent.apps.1234567890123-abcdef') 329082815dcSEvan Bacon ) 330082815dcSEvan Bacon ).toBeDefined(); 331082815dcSEvan Bacon // Branch 332082815dcSEvan Bacon expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 333082815dcSEvan Bacon 334082815dcSEvan Bacon // Mods should all be functions 3355f51f606SEvan Bacon expect(Object.values(config.mods!.ios!).every((value) => typeof value === 'function')).toBe( 3365f51f606SEvan Bacon true 3375f51f606SEvan Bacon ); 338082815dcSEvan Bacon 339082815dcSEvan Bacon delete config.mods; 340082815dcSEvan Bacon 341082815dcSEvan Bacon // Shape 342082815dcSEvan Bacon expect(config).toMatchSnapshot(); 343082815dcSEvan Bacon 344082815dcSEvan Bacon // Test the written files... 345082815dcSEvan Bacon const after = getDirFromFS(vol.toJSON(), projectRoot); 346082815dcSEvan Bacon 347ed3bd27bSEvan Bacon expect(Object.keys(after)).toEqual([ 348082815dcSEvan Bacon 'node_modules/react-native-maps/package.json', 349ed3bd27bSEvan Bacon 'ios/.xcode.env', 350ed3bd27bSEvan Bacon 'ios/HelloWorld/AppDelegate.h', 351ed3bd27bSEvan Bacon 'ios/HelloWorld/AppDelegate.mm', 352ed3bd27bSEvan Bacon 'ios/HelloWorld/Images.xcassets/AppIcon.appiconset/Contents.json', 353ed3bd27bSEvan Bacon 'ios/HelloWorld/Images.xcassets/Contents.json', 354ed3bd27bSEvan Bacon 'ios/HelloWorld/Images.xcassets/SplashScreenBackground.imageset/image.png', 355ed3bd27bSEvan Bacon 'ios/HelloWorld/Images.xcassets/SplashScreenBackground.imageset/Contents.json', 356ed3bd27bSEvan Bacon 'ios/HelloWorld/Info.plist', 357ed3bd27bSEvan Bacon 'ios/HelloWorld/SplashScreen.storyboard', 358ed3bd27bSEvan Bacon 'ios/HelloWorld/Supporting/Expo.plist', 359ed3bd27bSEvan Bacon 'ios/HelloWorld/Supporting/en.lproj/InfoPlist.strings', 360ed3bd27bSEvan Bacon 'ios/HelloWorld/Supporting/es.lproj/InfoPlist.strings', 361ed3bd27bSEvan Bacon 'ios/HelloWorld/main.m', 362ed3bd27bSEvan Bacon 'ios/HelloWorld/GoogleService-Info.plist', 363ed3bd27bSEvan Bacon 'ios/HelloWorld/noop-file.swift', 364ed3bd27bSEvan Bacon 'ios/HelloWorld/HelloWorld-Bridging-Header.h', 365ed3bd27bSEvan Bacon 'ios/HelloWorld/mycoolapp.entitlements', 366ed3bd27bSEvan Bacon 'ios/HelloWorld.xcodeproj/project.pbxproj', 367ed3bd27bSEvan Bacon 'ios/HelloWorld.xcodeproj/project.xcworkspace/contents.xcworkspacedata', 368ed3bd27bSEvan Bacon 'ios/HelloWorld.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 369ed3bd27bSEvan Bacon 'ios/HelloWorld.xcodeproj/xcshareddata/xcschemes/HelloWorld.xcscheme', 370ed3bd27bSEvan Bacon 'ios/HelloWorld.xcworkspace/contents.xcworkspacedata', 371ed3bd27bSEvan Bacon 'ios/HelloWorld.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 372082815dcSEvan Bacon 'ios/Podfile', 373ed3bd27bSEvan Bacon 'ios/Podfile.properties.json', 374ed3bd27bSEvan Bacon 'ios/gitignore', 375ed3bd27bSEvan Bacon 'android/app/build.gradle', 376ed3bd27bSEvan Bacon 'android/app/debug.keystore', 377ed3bd27bSEvan Bacon 'android/app/proguard-rules.pro', 378ed3bd27bSEvan Bacon 'android/app/src/debug/AndroidManifest.xml', 379ed3bd27bSEvan Bacon 'android/app/src/debug/java/com/bacon/todo/ReactNativeFlipper.java', 380ed3bd27bSEvan Bacon 'android/app/src/main/AndroidManifest.xml', 381082815dcSEvan Bacon 'android/app/src/main/java/com/bacon/todo/MainActivity.java', 382082815dcSEvan Bacon 'android/app/src/main/java/com/bacon/todo/MainApplication.java', 383ed3bd27bSEvan Bacon 'android/app/src/main/res/drawable/rn_edit_text_material.xml', 384ed3bd27bSEvan Bacon 'android/app/src/main/res/drawable/splashscreen.xml', 385082815dcSEvan Bacon 'android/app/src/main/res/values/colors.xml', 386082815dcSEvan Bacon 'android/app/src/main/res/values/strings.xml', 387ed3bd27bSEvan Bacon 'android/app/src/main/res/values/styles.xml', 388082815dcSEvan Bacon 'android/app/src/main/res/values-night/colors.xml', 389ed3bd27bSEvan Bacon 'android/app/src/release/java/com/bacon/todo/ReactNativeFlipper.java', 390082815dcSEvan Bacon 'android/app/google-services.json', 391082815dcSEvan Bacon 'android/build.gradle', 392ed3bd27bSEvan Bacon 'android/gitignore', 393ed3bd27bSEvan Bacon 'android/gradle/wrapper/gradle-wrapper.jar', 394ed3bd27bSEvan Bacon 'android/gradle/wrapper/gradle-wrapper.properties', 395ed3bd27bSEvan Bacon 'android/gradle.properties', 396ed3bd27bSEvan Bacon 'android/gradlew', 397ed3bd27bSEvan Bacon 'android/gradlew.bat', 398ed3bd27bSEvan Bacon 'android/settings.gradle', 399082815dcSEvan Bacon 'config/GoogleService-Info.plist', 400082815dcSEvan Bacon 'config/google-services.json', 401082815dcSEvan Bacon 'locales/en-US.json', 402082815dcSEvan Bacon ]); 403082815dcSEvan Bacon 404ed3bd27bSEvan Bacon expect(after['ios/HelloWorld/mycoolapp.entitlements']).toMatch( 405082815dcSEvan Bacon 'com.apple.developer.associated-domains' 406082815dcSEvan Bacon ); 407082815dcSEvan Bacon 408ed3bd27bSEvan Bacon expect(after['ios/HelloWorld/Info.plist']).toMatch(/com.bacon.todo/); 409ed3bd27bSEvan Bacon expect(after['ios/HelloWorld/Supporting/en.lproj/InfoPlist.strings']).toMatch( 410082815dcSEvan Bacon /foo = "uhh bar"/ 411082815dcSEvan Bacon ); 412ed3bd27bSEvan Bacon expect(after['ios/HelloWorld/GoogleService-Info.plist']).toBe(googleServiceInfoFixture); 413082815dcSEvan Bacon 414082815dcSEvan Bacon expect(after['android/app/src/main/java/com/bacon/todo/MainApplication.java']).toMatch( 415082815dcSEvan Bacon 'package com.bacon.todo;' 416082815dcSEvan Bacon ); 417082815dcSEvan Bacon 418082815dcSEvan Bacon expect(after['android/app/src/main/res/values/strings.xml']).toMatch( 419082815dcSEvan Bacon '<string name="app_name">my cool app</string>' 420082815dcSEvan Bacon ); 421082815dcSEvan Bacon 422082815dcSEvan Bacon // Ensure files are always written in the correct format 423082815dcSEvan Bacon for (const xmlPath of [ 424082815dcSEvan Bacon 'android/app/src/main/AndroidManifest.xml', 425082815dcSEvan Bacon 'android/app/src/main/res/values/styles.xml', 426082815dcSEvan Bacon 'android/app/src/main/res/values/strings.xml', 427082815dcSEvan Bacon 'android/app/src/main/res/values/colors.xml', 428ed3bd27bSEvan Bacon 'ios/HelloWorld/Info.plist', 429082815dcSEvan Bacon ]) { 430082815dcSEvan Bacon const isValid = await isValidXMLAsync(path.join(projectRoot, xmlPath)); 431082815dcSEvan Bacon if (!isValid) throw new Error(`Invalid XML file format at: "${xmlPath}"`); 432082815dcSEvan Bacon } 433082815dcSEvan Bacon 434082815dcSEvan Bacon // Ensure the infoPlist object is merged correctly 435082815dcSEvan Bacon const infoPlist = await plist.parse( 436ed3bd27bSEvan Bacon fs.readFileSync(path.join(projectRoot, 'ios/HelloWorld/Info.plist'), 'utf8') 437082815dcSEvan Bacon ); 438082815dcSEvan Bacon expect(infoPlist.bar).toStrictEqual({ val: ['foo'] }); 439082815dcSEvan Bacon // Ensure the entitlements object is merged correctly 440082815dcSEvan Bacon const entitlements = await plist.parse( 441ed3bd27bSEvan Bacon fs.readFileSync(path.join(projectRoot, 'ios/HelloWorld/mycoolapp.entitlements'), 'utf8') 442082815dcSEvan Bacon ); 443082815dcSEvan Bacon expect(entitlements.foo).toStrictEqual('bar'); 444082815dcSEvan Bacon 445082815dcSEvan Bacon // Ensure files are always written in the correct format 446082815dcSEvan Bacon for (const xmlPath of [ 447ed3bd27bSEvan Bacon 'ios/HelloWorld/Images.xcassets/AppIcon.appiconset/Contents.json', 448ed3bd27bSEvan Bacon 'ios/HelloWorld/Images.xcassets/Contents.json', 449082815dcSEvan Bacon 'android/app/google-services.json', 450082815dcSEvan Bacon ]) { 451082815dcSEvan Bacon const isValid = await isValidJSONAsync(path.join(projectRoot, xmlPath)); 452082815dcSEvan Bacon if (!isValid) throw new Error(`Invalid JSON file format at: "${xmlPath}"`); 453082815dcSEvan Bacon } 454082815dcSEvan Bacon 455082815dcSEvan Bacon // Ensure the Xcode project file can be read and parsed. 456082815dcSEvan Bacon const project = xcode.project( 457ed3bd27bSEvan Bacon path.join(projectRoot, 'ios/HelloWorld.xcodeproj/project.pbxproj') 458082815dcSEvan Bacon ); 459082815dcSEvan Bacon project.parseSync(); 460082815dcSEvan Bacon }); 461082815dcSEvan Bacon 462082815dcSEvan Bacon it('introspects mods', async () => { 463082815dcSEvan Bacon let config = getPrebuildConfig(); 464082815dcSEvan Bacon 465082815dcSEvan Bacon // Apply mod 466082815dcSEvan Bacon config = await compileModsAsync(config, { introspect: true, projectRoot: '/app' }); 467082815dcSEvan Bacon 468082815dcSEvan Bacon // App config should have been modified 469082815dcSEvan Bacon expect(config.name).toBe('my cool app'); 4705f51f606SEvan Bacon expect(config.ios?.infoPlist).toBeDefined(); 4715f51f606SEvan Bacon expect(config.ios?.entitlements).toBeDefined(); 472082815dcSEvan Bacon 473082815dcSEvan Bacon // Google Sign In 474082815dcSEvan Bacon expect( 475082815dcSEvan Bacon config.ios?.infoPlist?.CFBundleURLTypes?.find(({ CFBundleURLSchemes }) => 4760060b409SEvan Bacon CFBundleURLSchemes.includes('com.googleusercontent.apps.1234567890123-abcdef') 477082815dcSEvan Bacon ) 478082815dcSEvan Bacon ).toBeDefined(); 479082815dcSEvan Bacon // Branch 480082815dcSEvan Bacon expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 481082815dcSEvan Bacon 4825f51f606SEvan Bacon const mods = config.mods!; 483082815dcSEvan Bacon // Mods should all be functions 4845f51f606SEvan Bacon expect(Object.values(mods.ios!).every((value) => typeof value === 'function')).toBe(true); 4855f51f606SEvan Bacon expect(Object.values(mods.android!).every((value) => typeof value === 'function')).toBe(true); 486082815dcSEvan Bacon // Ensure these mods are removed 4875f51f606SEvan Bacon expect(mods.android?.dangerous).toBeUndefined(); 4885f51f606SEvan Bacon expect(mods.android?.mainActivity).toBeUndefined(); 4895f51f606SEvan Bacon expect(mods.android?.appBuildGradle).toBeUndefined(); 4905f51f606SEvan Bacon expect(mods.android?.projectBuildGradle).toBeUndefined(); 4915f51f606SEvan Bacon expect(mods.android?.settingsGradle).toBeUndefined(); 4925f51f606SEvan Bacon expect(mods.ios?.dangerous).toBeUndefined(); 4935f51f606SEvan Bacon expect(mods.ios?.xcodeproj).toBeUndefined(); 494082815dcSEvan Bacon 495082815dcSEvan Bacon delete config.mods; 496082815dcSEvan Bacon 497082815dcSEvan Bacon // Shape 498082815dcSEvan Bacon expect(config).toMatchSnapshot(); 499082815dcSEvan Bacon 5005f51f606SEvan Bacon expect(config._internal?.modResults).toBeDefined(); 5015f51f606SEvan Bacon expect(config._internal?.modResults.ios.infoPlist).toBeDefined(); 5025f51f606SEvan Bacon expect(config._internal?.modResults.ios.expoPlist).toBeDefined(); 5035f51f606SEvan Bacon expect(config._internal?.modResults.ios.entitlements).toBeDefined(); 5045f51f606SEvan Bacon expect(config._internal?.modResults.android.manifest).toBeDefined(); 5055f51f606SEvan Bacon expect(Array.isArray(config._internal?.modResults.android.gradleProperties)).toBe(true); 5065f51f606SEvan Bacon expect(config._internal?.modResults.android.strings).toBeDefined(); 507082815dcSEvan Bacon 508082815dcSEvan Bacon // Test the written files... 509082815dcSEvan Bacon const after = getDirFromFS(vol.toJSON(), projectRoot); 510082815dcSEvan Bacon 511ed3bd27bSEvan Bacon expect(Object.keys(after)).toEqual([ 512082815dcSEvan Bacon 'node_modules/react-native-maps/package.json', 513ed3bd27bSEvan Bacon 'ios/.xcode.env', 514ed3bd27bSEvan Bacon 'ios/HelloWorld/AppDelegate.h', 515ed3bd27bSEvan Bacon 'ios/HelloWorld/AppDelegate.mm', 516ed3bd27bSEvan Bacon 'ios/HelloWorld/Images.xcassets/AppIcon.appiconset/Contents.json', 517ed3bd27bSEvan Bacon 'ios/HelloWorld/Images.xcassets/Contents.json', 518ed3bd27bSEvan Bacon 'ios/HelloWorld/Images.xcassets/SplashScreen.imageset/Contents.json', 519ed3bd27bSEvan Bacon 'ios/HelloWorld/Images.xcassets/SplashScreenBackground.imageset/Contents.json', 520ed3bd27bSEvan Bacon 'ios/HelloWorld/Info.plist', 521ed3bd27bSEvan Bacon 'ios/HelloWorld/SplashScreen.storyboard', 522ed3bd27bSEvan Bacon 'ios/HelloWorld/Supporting/Expo.plist', 523ed3bd27bSEvan Bacon 'ios/HelloWorld/main.m', 524ed3bd27bSEvan Bacon 'ios/HelloWorld/HelloWorld.entitlements', 525ed3bd27bSEvan Bacon 'ios/HelloWorld.xcodeproj/project.pbxproj', 526ed3bd27bSEvan Bacon 'ios/HelloWorld.xcodeproj/project.xcworkspace/contents.xcworkspacedata', 527ed3bd27bSEvan Bacon 'ios/HelloWorld.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 528ed3bd27bSEvan Bacon 'ios/HelloWorld.xcodeproj/xcshareddata/xcschemes/HelloWorld.xcscheme', 529ed3bd27bSEvan Bacon 'ios/HelloWorld.xcworkspace/contents.xcworkspacedata', 530ed3bd27bSEvan Bacon 'ios/HelloWorld.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 531082815dcSEvan Bacon 'ios/Podfile', 532ed3bd27bSEvan Bacon 'ios/Podfile.properties.json', 533ed3bd27bSEvan Bacon 'ios/gitignore', 534082815dcSEvan Bacon 'android/app/build.gradle', 535ed3bd27bSEvan Bacon 'android/app/debug.keystore', 536ed3bd27bSEvan Bacon 'android/app/proguard-rules.pro', 537ed3bd27bSEvan Bacon 'android/app/src/debug/AndroidManifest.xml', 538ed3bd27bSEvan Bacon 'android/app/src/debug/java/com/helloworld/ReactNativeFlipper.java', 539ed3bd27bSEvan Bacon 'android/app/src/main/AndroidManifest.xml', 540ed3bd27bSEvan Bacon 'android/app/src/main/java/com/helloworld/MainActivity.java', 541ed3bd27bSEvan Bacon 'android/app/src/main/java/com/helloworld/MainApplication.java', 542ed3bd27bSEvan Bacon 'android/app/src/main/res/drawable/rn_edit_text_material.xml', 543ed3bd27bSEvan Bacon 'android/app/src/main/res/drawable/splashscreen.xml', 544ed3bd27bSEvan Bacon 'android/app/src/main/res/values/colors.xml', 545ed3bd27bSEvan Bacon 'android/app/src/main/res/values/strings.xml', 546ed3bd27bSEvan Bacon 'android/app/src/main/res/values/styles.xml', 547ed3bd27bSEvan Bacon 'android/app/src/release/java/com/helloworld/ReactNativeFlipper.java', 548082815dcSEvan Bacon 'android/build.gradle', 549ed3bd27bSEvan Bacon 'android/gitignore', 550ed3bd27bSEvan Bacon 'android/gradle/wrapper/gradle-wrapper.jar', 551ed3bd27bSEvan Bacon 'android/gradle/wrapper/gradle-wrapper.properties', 552ed3bd27bSEvan Bacon 'android/gradle.properties', 553ed3bd27bSEvan Bacon 'android/gradlew', 554ed3bd27bSEvan Bacon 'android/gradlew.bat', 555ed3bd27bSEvan Bacon 'android/settings.gradle', 556082815dcSEvan Bacon 'config/GoogleService-Info.plist', 557082815dcSEvan Bacon 'config/google-services.json', 558082815dcSEvan Bacon 'locales/en-US.json', 559082815dcSEvan Bacon ]); 560082815dcSEvan Bacon 561082815dcSEvan Bacon // unmodified 562ed3bd27bSEvan Bacon expect(after['ios/HelloWorld/HelloWorld.entitlements']).not.toMatch( 563082815dcSEvan Bacon 'com.apple.developer.associated-domains' 564082815dcSEvan Bacon ); 565082815dcSEvan Bacon 566ed3bd27bSEvan Bacon expect(after['ios/HelloWorld/Info.plist']).toBe(rnFixture['ios/HelloWorld/Info.plist']); 567082815dcSEvan Bacon 568ed3bd27bSEvan Bacon expect(after['android/app/src/main/java/com/helloworld/MainApplication.java']).toBe( 569ed3bd27bSEvan Bacon rnFixture['android/app/src/main/java/com/helloworld/MainApplication.java'] 570082815dcSEvan Bacon ); 571ed3bd27bSEvan Bacon expect(after['android/app/src/main/java/com/helloworld/MainActivity.java']).toBe( 572ed3bd27bSEvan Bacon rnFixture['android/app/src/main/java/com/helloworld/MainActivity.java'] 573082815dcSEvan Bacon ); 574082815dcSEvan Bacon expect(after['android/app/src/main/res/values/styles.xml']).toMatch( 575082815dcSEvan Bacon rnFixture['android/app/src/main/res/values/styles.xml'] 576082815dcSEvan Bacon ); 577082815dcSEvan Bacon 578ed3bd27bSEvan Bacon // for (const [name, contents] of Object.entries(rnFixture)) { 579ed3bd27bSEvan Bacon // // The pbxproj seems to reformat in jest 580ed3bd27bSEvan Bacon // if (name.includes('pbxproj') || name.endsWith('MainApplicationTurboModuleManagerDelegate.h')) 581ed3bd27bSEvan Bacon // continue; 582ed3bd27bSEvan Bacon // expect(after[name]).toMatch(contents); 583ed3bd27bSEvan Bacon // } 584082815dcSEvan Bacon // Ensure the Xcode project file can be read and parsed. 585082815dcSEvan Bacon const project = xcode.project( 586ed3bd27bSEvan Bacon path.join(projectRoot, 'ios/HelloWorld.xcodeproj/project.pbxproj') 587082815dcSEvan Bacon ); 588082815dcSEvan Bacon project.parseSync(); 589082815dcSEvan Bacon }); 590082815dcSEvan Bacon 591082815dcSEvan Bacon // Tests that introspection works 592082815dcSEvan Bacon it('introspects mods in a managed project', async () => { 593082815dcSEvan Bacon vol.reset(); 594082815dcSEvan Bacon vol.fromJSON( 595082815dcSEvan Bacon { 596082815dcSEvan Bacon // Required to link react-native-maps 597082815dcSEvan Bacon './node_modules/react-native-maps/package.json': JSON.stringify({}), 598082815dcSEvan Bacon // App files 5990060b409SEvan Bacon 'config/GoogleService-Info.plist': googleServiceInfoFixture, 600082815dcSEvan Bacon 'config/google-services.json': '{}', 601082815dcSEvan Bacon 'icons/foreground.png': icon, 602082815dcSEvan Bacon 'icons/background.png': icon, 603082815dcSEvan Bacon 'icons/notification-icon.png': icon, 604082815dcSEvan Bacon 'icons/ios-icon.png': icon, 605082815dcSEvan Bacon 'locales/en-US.json': JSON.stringify({ foo: 'uhh bar', fallback: 'fallback' }, null, 2), 606082815dcSEvan Bacon }, 607082815dcSEvan Bacon projectRoot 608082815dcSEvan Bacon ); 609082815dcSEvan Bacon 610082815dcSEvan Bacon let config = getPrebuildConfig(); 611082815dcSEvan Bacon 612082815dcSEvan Bacon // Apply mod 613082815dcSEvan Bacon config = await compileModsAsync(config, { introspect: true, projectRoot: '/app' }); 614082815dcSEvan Bacon 615082815dcSEvan Bacon // App config should have been modified 616082815dcSEvan Bacon expect(config.name).toBe('my cool app'); 6175f51f606SEvan Bacon expect(config.ios?.infoPlist).toBeDefined(); 6185f51f606SEvan Bacon expect(config.ios?.entitlements).toBeDefined(); 619082815dcSEvan Bacon 620082815dcSEvan Bacon // Google Sign In 621082815dcSEvan Bacon expect( 622082815dcSEvan Bacon config.ios?.infoPlist?.CFBundleURLTypes?.find(({ CFBundleURLSchemes }) => 6230060b409SEvan Bacon CFBundleURLSchemes.includes('com.googleusercontent.apps.1234567890123-abcdef') 624082815dcSEvan Bacon ) 625082815dcSEvan Bacon ).toBeDefined(); 626082815dcSEvan Bacon // Branch 627082815dcSEvan Bacon expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 628082815dcSEvan Bacon 6295f51f606SEvan Bacon const mods = config.mods!; 630082815dcSEvan Bacon // Mods should all be functions 6315f51f606SEvan Bacon expect(Object.values(mods.ios!).every((value) => typeof value === 'function')).toBe(true); 6325f51f606SEvan Bacon expect(Object.values(mods.android!).every((value) => typeof value === 'function')).toBe(true); 633082815dcSEvan Bacon // Ensure these mods are removed 6345f51f606SEvan Bacon expect(mods.android?.dangerous).toBeUndefined(); 6355f51f606SEvan Bacon expect(mods.android?.mainActivity).toBeUndefined(); 6365f51f606SEvan Bacon expect(mods.android?.appBuildGradle).toBeUndefined(); 6375f51f606SEvan Bacon expect(mods.android?.projectBuildGradle).toBeUndefined(); 6385f51f606SEvan Bacon expect(mods.android?.settingsGradle).toBeUndefined(); 6395f51f606SEvan Bacon expect(mods.ios?.dangerous).toBeUndefined(); 6405f51f606SEvan Bacon expect(mods.ios?.xcodeproj).toBeUndefined(); 641082815dcSEvan Bacon 642082815dcSEvan Bacon delete config.mods; 643082815dcSEvan Bacon 644082815dcSEvan Bacon // Shape 645082815dcSEvan Bacon expect(config).toMatchSnapshot(); 646082815dcSEvan Bacon 6475f51f606SEvan Bacon expect(config._internal?.modResults).toBeDefined(); 6485f51f606SEvan Bacon expect(config._internal?.modResults.ios.infoPlist).toBeDefined(); 6495f51f606SEvan Bacon expect(config._internal?.modResults.ios.expoPlist).toBeDefined(); 6505f51f606SEvan Bacon expect(config._internal?.modResults.ios.entitlements).toBeDefined(); 6515f51f606SEvan Bacon expect(config._internal?.modResults.android.manifest).toBeDefined(); 6525f51f606SEvan Bacon expect(Array.isArray(config._internal?.modResults.android.gradleProperties)).toBe(true); 6535f51f606SEvan Bacon expect(config._internal?.modResults.android.strings).toBeDefined(); 654082815dcSEvan Bacon 655082815dcSEvan Bacon // Test the written files... 656082815dcSEvan Bacon const after = getDirFromFS(vol.toJSON(), projectRoot); 657082815dcSEvan Bacon 658082815dcSEvan Bacon expect(Object.keys(after)).toStrictEqual([ 659082815dcSEvan Bacon 'node_modules/react-native-maps/package.json', 660082815dcSEvan Bacon 'config/GoogleService-Info.plist', 661082815dcSEvan Bacon 'config/google-services.json', 662082815dcSEvan Bacon 'locales/en-US.json', 663082815dcSEvan Bacon ]); 664082815dcSEvan Bacon }); 665082815dcSEvan Bacon 666082815dcSEvan Bacon it('create Podfile.properties.json file for backward compatible', async () => { 667082815dcSEvan Bacon const { '/app/ios/Podfile.properties.json': _, ...volWithoutPodfileProperties } = vol.toJSON(); 668082815dcSEvan Bacon vol.reset(); 669082815dcSEvan Bacon vol.fromJSON(volWithoutPodfileProperties); 670082815dcSEvan Bacon 671082815dcSEvan Bacon let config = getPrebuildConfig(); 672082815dcSEvan Bacon // change jsEngine to hermes 673082815dcSEvan Bacon config.jsEngine = 'hermes'; 674082815dcSEvan Bacon 675082815dcSEvan Bacon config = await compileModsAsync(config, { projectRoot: '/app' }); 676082815dcSEvan Bacon 677082815dcSEvan Bacon const result = await JsonFile.readAsync('/app/ios/Podfile.properties.json'); 678082815dcSEvan Bacon expect(result).toMatchObject({ 'expo.jsEngine': 'hermes' }); 679082815dcSEvan Bacon }); 680082815dcSEvan Bacon}); 681082815dcSEvan Bacon 682082815dcSEvan Baconasync function isValidXMLAsync(filePath: string) { 683082815dcSEvan Bacon try { 684082815dcSEvan Bacon const res = await readXMLAsync({ path: filePath }); 685082815dcSEvan Bacon return !!res; 686082815dcSEvan Bacon } catch { 687082815dcSEvan Bacon return false; 688082815dcSEvan Bacon } 689082815dcSEvan Bacon} 690082815dcSEvan Bacon 691082815dcSEvan Baconasync function isValidJSONAsync(filePath: string) { 692082815dcSEvan Bacon try { 693082815dcSEvan Bacon const res = await JsonFile.readAsync(filePath); 694082815dcSEvan Bacon return !!res; 695082815dcSEvan Bacon } catch { 696082815dcSEvan Bacon return false; 697082815dcSEvan Bacon } 698082815dcSEvan Bacon} 699