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