1import { AndroidConfig } from '@expo/config-plugins';
2import { ExpoConfig } from '@expo/config-types';
3import * as fs from 'fs';
4import { vol } from 'memfs';
5import * as path from 'path';
6
7import {
8  ADAPTIVE_ICON_XML_WITH_BACKGROUND_COLOR,
9  ADAPTIVE_ICON_XML_WITH_BACKGROUND_COLOR_AND_MONOCHROME,
10  ADAPTIVE_ICON_XML_WITH_BOTH,
11  ADAPTIVE_ICON_XML_WITH_BOTH_AND_MONOCHROME,
12  LIST_OF_ANDROID_ADAPTIVE_ICON_FILES_FINAL,
13  SAMPLE_COLORS_XML,
14} from '../../__tests__/fixtures/androidIcons';
15import rnFixture from '../../__tests__/fixtures/react-native-project';
16import { getDirFromFS } from '../../__tests__/getDirFromFS';
17import {
18  createAdaptiveIconXmlString,
19  getAdaptiveIcon,
20  getIcon,
21  setIconAsync,
22  setRoundIconManifest,
23} from '../withAndroidIcons';
24
25const { getMainApplicationOrThrow, readAndroidManifestAsync } = AndroidConfig.Manifest;
26const fsReal = jest.requireActual('fs') as typeof fs;
27
28jest.mock('fs');
29
30function setUpMipmapDirectories() {
31  vol.mkdirpSync('/app/android/app/src/main/res/mipmap-mdpi');
32  vol.mkdirpSync('/app/android/app/src/main/res/mipmap-hdpi');
33  vol.mkdirpSync('/app/android/app/src/main/res/mipmap-xhdpi');
34  vol.mkdirpSync('/app/android/app/src/main/res/mipmap-xxhdpi');
35  vol.mkdirpSync('/app/android/app/src/main/res/mipmap-xxxhdpi');
36}
37
38describe(setRoundIconManifest, () => {
39  vol.fromJSON({ './AndroidManifest.xml': rnFixture['android/app/src/main/AndroidManifest.xml'] });
40  it(`adds the round icon property when an adaptive icon is present`, async () => {
41    const manifest = await readAndroidManifestAsync('./AndroidManifest.xml');
42    const results = setRoundIconManifest({ android: { adaptiveIcon: {} } }, manifest);
43
44    const app = getMainApplicationOrThrow(results);
45    expect(app.$['android:roundIcon']).toBe('@mipmap/ic_launcher_round');
46  });
47  it(`removes the round icon property when an adaptive icon is missing`, async () => {
48    const manifest = await readAndroidManifestAsync('./AndroidManifest.xml');
49    const results = setRoundIconManifest({ android: {} }, manifest);
50
51    const app = getMainApplicationOrThrow(results);
52    expect(app.$['android:roundIcon']).not.toBeDefined();
53  });
54});
55
56describe('Android Icon', () => {
57  it(`returns null if no icon values provided`, () => {
58    expect(getIcon({} as ExpoConfig)).toBeNull();
59    expect(getAdaptiveIcon({} as ExpoConfig)).toMatchObject({
60      foregroundImage: null,
61      backgroundColor: null,
62      backgroundImage: null,
63    });
64  });
65
66  it(`returns adaptive icon over android icon`, () => {
67    const config = {
68      icon: 'icon',
69      android: {
70        icon: 'androidIcon',
71        adaptiveIcon: {
72          foregroundImage: 'adaptiveIcon',
73          backgroundImage: 'backgroundImage',
74          backgroundColor: '#000000',
75        },
76      },
77    };
78    const { foregroundImage, backgroundColor, backgroundImage } = getAdaptiveIcon(
79      config as ExpoConfig
80    );
81    const icon = foregroundImage || getIcon(config as ExpoConfig);
82    expect(icon).toMatch('adaptiveIcon');
83    expect(backgroundColor).toMatch('#000000');
84    expect(backgroundImage).toMatch('backgroundImage');
85  });
86
87  it(`creates the proper AdaptiveIconXmlString`, () => {
88    const withBackgroundImage = createAdaptiveIconXmlString('path/to/image', null);
89    const withBackgroundColor = createAdaptiveIconXmlString(null, null);
90    const withBackgroundColorAndMonochrome = createAdaptiveIconXmlString(null, 'path/to/image');
91    const withBoth = createAdaptiveIconXmlString('path/to/image', null);
92    const withBothAndMonochrome = createAdaptiveIconXmlString('path/to/image', 'path/to/image');
93
94    expect(withBackgroundColor).toBe(ADAPTIVE_ICON_XML_WITH_BACKGROUND_COLOR);
95    expect(withBackgroundColorAndMonochrome).toBe(
96      ADAPTIVE_ICON_XML_WITH_BACKGROUND_COLOR_AND_MONOCHROME
97    );
98    expect(withBackgroundImage).toBe(ADAPTIVE_ICON_XML_WITH_BOTH);
99    expect(withBoth).toBe(ADAPTIVE_ICON_XML_WITH_BOTH);
100    expect(withBothAndMonochrome).toBe(ADAPTIVE_ICON_XML_WITH_BOTH_AND_MONOCHROME);
101  });
102
103  it('returns null if no icon config provided', async () => {
104    expect(
105      await setIconAsync('./', {
106        icon: null,
107        backgroundImage: null,
108        backgroundColor: null,
109        isAdaptive: false,
110        monochromeImage: null,
111      })
112    ).toBe(null);
113  });
114});
115
116describe('e2e: ONLY android legacy icon', () => {
117  const legacyIconPath = path.resolve(__dirname, '../../__tests__/fixtures/icon.png');
118  const projectRoot = '/app';
119  const icon = require('../../icons/withAndroidIcons');
120  const spyOnConfigureAdaptiveIconAsync = jest.spyOn(icon, 'configureAdaptiveIconAsync');
121  beforeAll(async () => {
122    const icon = fsReal.readFileSync(legacyIconPath);
123    vol.fromJSON(
124      { './android/app/src/main/res/values/colors.xml': SAMPLE_COLORS_XML },
125      projectRoot
126    );
127    setUpMipmapDirectories();
128    vol.mkdirpSync('/app/assets');
129    vol.writeFileSync('/app/assets/iconForeground.png', icon);
130
131    await setIconAsync(projectRoot, {
132      icon: '/app/assets/iconForeground.png',
133      backgroundColor: null,
134      backgroundImage: null,
135      isAdaptive: true,
136      monochromeImage: null,
137    });
138  });
139
140  afterAll(() => {
141    vol.reset();
142  });
143
144  it('writes all the image files expected', async () => {
145    const after = getDirFromFS(vol.toJSON(), projectRoot);
146    Object.keys(after).forEach((path) => {
147      expect(LIST_OF_ANDROID_ADAPTIVE_ICON_FILES_FINAL).toContain(path);
148    });
149  });
150
151  it('Does not set adaptive icon config', () => {
152    expect(spyOnConfigureAdaptiveIconAsync).toHaveBeenCalledTimes(0);
153  });
154});
155
156describe('e2e: android adaptive icon', () => {
157  const adaptiveIconForegroundPath = path.resolve(__dirname, '../../__tests__/fixtures/icon.png');
158  const adaptiveIconBackgroundPath = path.resolve(__dirname, '../../__tests__/fixtures/icon.png');
159  const adaptiveIconMonochromePath = path.resolve(__dirname, '../../__tests__/fixtures/icon.png');
160  const projectRoot = '/app';
161
162  beforeAll(async () => {
163    const adaptiveIconForeground = fsReal.readFileSync(adaptiveIconForegroundPath);
164    const adaptiveIconBackground = fsReal.readFileSync(adaptiveIconBackgroundPath);
165    const adaptiveIconMonochrome = fsReal.readFileSync(adaptiveIconMonochromePath);
166
167    vol.fromJSON(
168      { './android/app/src/main/res/values/colors.xml': SAMPLE_COLORS_XML },
169      projectRoot
170    );
171    setUpMipmapDirectories();
172    vol.mkdirpSync('/app/assets');
173    vol.writeFileSync('/app/assets/iconForeground.png', adaptiveIconForeground);
174    vol.writeFileSync('/app/assets/iconBackground.png', adaptiveIconBackground);
175    vol.writeFileSync('/app/assets/iconMonochrome.png', adaptiveIconMonochrome);
176
177    await setIconAsync(projectRoot, {
178      icon: '/app/assets/iconForeground.png',
179      backgroundImage: '/app/assets/iconBackground.png',
180      backgroundColor: '#123456',
181      isAdaptive: true,
182      monochromeImage: '/app/assets/iconMonochrome.png',
183    });
184  });
185
186  afterAll(() => {
187    vol.reset();
188  });
189
190  it('writes all the image files expected', () => {
191    const after = getDirFromFS(vol.toJSON(), projectRoot);
192    expect(Object.keys(after)).toEqual(LIST_OF_ANDROID_ADAPTIVE_ICON_FILES_FINAL);
193  });
194});
195