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