1import fs from 'fs';
2import { vol } from 'memfs';
3
4import { ExportedConfig, Mod } from '../../Plugin.types';
5import { compileModsAsync } from '../mod-compiler';
6import { withMod } from '../withMod';
7import rnFixture from './fixtures/react-native-project';
8
9jest.mock('fs');
10
11describe(compileModsAsync, () => {
12  const projectRoot = '/app';
13  const originalWarn = console.warn;
14  beforeEach(async () => {
15    console.warn = jest.fn();
16    // Trick XDL Info.plist reading
17    Object.defineProperty(process, 'platform', {
18      value: 'not-darwin',
19    });
20    vol.fromJSON(rnFixture, projectRoot);
21  });
22
23  afterEach(() => {
24    console.warn = originalWarn;
25    vol.reset();
26  });
27
28  it('skips missing providers in loose mode', async () => {
29    // A basic plugin exported from an app.json
30    let exportedConfig: ExportedConfig = {
31      name: 'app',
32      slug: '',
33      mods: null,
34    };
35
36    const action: Mod<any> = jest.fn((props) => {
37      // Capitalize app name
38      props.name = (props.name as string).toUpperCase();
39      return props;
40    });
41    // Apply mod
42    exportedConfig = withMod<any>(exportedConfig, {
43      platform: 'android',
44      mod: 'custom',
45      action,
46    });
47
48    const config = await compileModsAsync(exportedConfig, {
49      projectRoot,
50      assertMissingModProviders: false,
51    });
52
53    expect(config.name).toBe('app');
54    // Base mods are skipped when no mods are applied, these shouldn't be defined.
55    expect(config.ios?.infoPlist).toBeUndefined();
56    expect(config.ios?.entitlements).toBeUndefined();
57    // Adds base mods
58    expect(Object.values(config.mods.ios).every((value) => typeof value === 'function')).toBe(true);
59
60    expect(action).not.toBeCalled();
61  });
62
63  it('asserts missing providers', async () => {
64    // A basic plugin exported from an app.json
65    let exportedConfig: ExportedConfig = {
66      name: 'app',
67      slug: '',
68      mods: null,
69    };
70
71    // Apply mod
72    exportedConfig = withMod<any>(exportedConfig, {
73      platform: 'android',
74      mod: 'custom',
75      action(config) {
76        return config;
77      },
78    });
79
80    await expect(
81      compileModsAsync(exportedConfig, { projectRoot, assertMissingModProviders: true })
82    ).rejects.toThrow(
83      `Initial base modifier for "android.custom" is not a provider and therefore will not provide modResults to child mods`
84    );
85  });
86
87  it('compiles with no mods', async () => {
88    // A basic plugin exported from an app.json
89    const exportedConfig: ExportedConfig = {
90      name: 'app',
91      slug: '',
92      mods: null,
93    };
94    const config = await compileModsAsync(exportedConfig, { projectRoot });
95
96    expect(config.name).toBe('app');
97    // Base mods are skipped when no mods are applied, these shouldn't be defined.
98    expect(config.ios?.infoPlist).toBeUndefined();
99    expect(config.ios?.entitlements).toBeUndefined();
100    // Adds base mods
101    expect(Object.values(config.mods.ios).every((value) => typeof value === 'function')).toBe(true);
102  });
103
104  it('compiles mods', async () => {
105    // A basic plugin exported from an app.json
106    let internalValue = '';
107    const exportedConfig: ExportedConfig = {
108      name: 'app',
109      slug: '',
110      mods: {
111        ios: {
112          async infoPlist(config) {
113            // Store the incoming value
114            internalValue = config.modResults.CFBundleDevelopmentRegion;
115            // Modify the data
116            config.modResults.CFBundleDevelopmentRegion =
117              'CFBundleDevelopmentRegion-crazy-random-value';
118            return config;
119          },
120        },
121      },
122    };
123
124    // Apply mod plugin
125    const config = await compileModsAsync(exportedConfig, { projectRoot });
126
127    expect(internalValue).toBe('$(DEVELOPMENT_LANGUAGE)');
128
129    // App config should have been modified
130    expect(config.name).toBe('app');
131    expect(config.ios.infoPlist).toBeDefined();
132    // No entitlements mod means this won't be defined
133    expect(config.ios.entitlements).toBeUndefined();
134
135    // Plugins should all be functions
136    expect(Object.values(config.mods.ios).every((value) => typeof value === 'function')).toBe(true);
137
138    // Test that the actual file was rewritten.
139    const data = await fs.promises.readFile('/app/ios/HelloWorld/Info.plist', 'utf8');
140    expect(data).toMatch(/CFBundleDevelopmentRegion-crazy-random-value/);
141  });
142
143  for (const invalid of [[{}], null, 7]) {
144    it(`throws on invalid mod results (${invalid})`, async () => {
145      // A basic plugin exported from an app.json
146      const exportedConfig: ExportedConfig = {
147        name: 'app',
148        slug: '',
149        mods: {
150          ios: {
151            async infoPlist(config) {
152              // Return an invalid config
153              return invalid as any;
154            },
155          },
156        },
157      };
158
159      // Apply mod plugin
160      await expect(compileModsAsync(exportedConfig, { projectRoot })).rejects.toThrow(
161        /Mod `mods.ios.infoPlist` evaluated to an object that is not a valid project config/
162      );
163    });
164  }
165});
166