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