xref: /expo/packages/jest-expo/src/preset/setup.js (revision 6e021b28)
1/**
2 * Adds Expo-related mocks to the Jest environment. Jest runs this setup module after it runs the
3 * React Native setup module.
4 */
5'use strict';
6
7const mockNativeModules = require('react-native/Libraries/BatchedBridge/NativeModules');
8
9const publicExpoModules = require('./expoModules');
10const internalExpoModules = require('./internalExpoModules');
11
12// window isn't defined as of react-native 0.45+ it seems
13if (typeof window !== 'object') {
14  globalThis.window = global;
15  globalThis.window.navigator = {};
16}
17
18const mockImageLoader = {
19  configurable: true,
20  enumerable: true,
21  get: () => ({
22    prefetchImage: jest.fn(),
23    getSize: jest.fn((uri, success) => process.nextTick(() => success(320, 240))),
24  }),
25};
26Object.defineProperty(mockNativeModules, 'ImageLoader', mockImageLoader);
27Object.defineProperty(mockNativeModules, 'ImageViewManager', mockImageLoader);
28
29Object.defineProperty(mockNativeModules, 'LinkingManager', {
30  configurable: true,
31  enumerable: true,
32  get: () => mockNativeModules.Linking,
33});
34
35const expoModules = {
36  ...publicExpoModules,
37  ...internalExpoModules,
38};
39
40function mock(property, customMock) {
41  const propertyType = property.type;
42  let mockValue;
43  if (customMock !== undefined) {
44    mockValue = customMock;
45  } else if (propertyType === 'function') {
46    if (property.functionType === 'promise') {
47      mockValue = jest.fn(() => Promise.resolve());
48    } else {
49      mockValue = jest.fn();
50    }
51  } else if (propertyType === 'number') {
52    mockValue = 1;
53  } else if (propertyType === 'string') {
54    mockValue = 'mock';
55  } else if (propertyType === 'array') {
56    mockValue = [];
57  } else if (propertyType === 'mock') {
58    mockValue = mockByMockDefinition(property.mockDefinition);
59  } else {
60    mockValue = {};
61  }
62  return mockValue;
63}
64
65function mockProperties(moduleProperties, customMocks) {
66  const mockedProperties = {};
67  for (const propertyName of Object.keys(moduleProperties)) {
68    const property = moduleProperties[propertyName];
69    const customMock =
70      customMocks && customMocks.hasOwnProperty(propertyName)
71        ? customMocks[propertyName]
72        : property.mock;
73    mockedProperties[propertyName] = mock(property, customMock);
74  }
75  return mockedProperties;
76}
77
78function mockByMockDefinition(definition) {
79  const mock = {};
80  for (const key of Object.keys(definition)) {
81    mock[key] = mockProperties(definition[key]);
82  }
83  return mock;
84}
85
86for (const moduleName of Object.keys(expoModules)) {
87  const moduleProperties = expoModules[moduleName];
88  const mockedProperties = mockProperties(moduleProperties);
89
90  Object.defineProperty(mockNativeModules, moduleName, {
91    configurable: true,
92    enumerable: true,
93    get: () => mockedProperties,
94  });
95}
96
97Object.keys(mockNativeModules.NativeUnimoduleProxy.viewManagersMetadata).forEach(
98  (viewManagerName) => {
99    Object.defineProperty(mockNativeModules.UIManager, `ViewManagerAdapter_${viewManagerName}`, {
100      get: () => ({
101        NativeProps: {},
102        directEventTypes: [],
103      }),
104    });
105  }
106);
107
108try {
109  jest.mock('expo-file-system', () => ({
110    downloadAsync: jest.fn(() => Promise.resolve({ md5: 'md5', uri: 'uri' })),
111    getInfoAsync: jest.fn(() => Promise.resolve({ exists: true, md5: 'md5', uri: 'uri' })),
112    readAsStringAsync: jest.fn(() => Promise.resolve()),
113    writeAsStringAsync: jest.fn(() => Promise.resolve()),
114    deleteAsync: jest.fn(() => Promise.resolve()),
115    moveAsync: jest.fn(() => Promise.resolve()),
116    copyAsync: jest.fn(() => Promise.resolve()),
117    makeDirectoryAsync: jest.fn(() => Promise.resolve()),
118    readDirectoryAsync: jest.fn(() => Promise.resolve()),
119    createDownloadResumable: jest.fn(() => Promise.resolve()),
120  }));
121} catch (error) {
122  // Allow this module to be optional for bare-workflow
123  if (error.code !== 'MODULE_NOT_FOUND') {
124    throw error;
125  }
126}
127
128jest.mock('react-native/Libraries/Image/AssetRegistry', () => ({
129  registerAsset: jest.fn(() => 1),
130  getAssetByID: jest.fn(() => ({
131    fileSystemLocation: '/full/path/to/directory',
132    httpServerLocation: '/assets/full/path/to/directory',
133    scales: [1],
134    fileHashes: ['md5'],
135    name: 'name',
136    exists: true,
137    type: 'type',
138    hash: 'md5',
139    uri: 'uri',
140    width: 1,
141    height: 1,
142  })),
143}));
144
145jest.doMock('react-native/Libraries/BatchedBridge/NativeModules', () => mockNativeModules);
146
147jest.doMock('react-native/Libraries/LogBox/LogBox', () => ({
148  ignoreLogs: (patterns) => {
149    // Do nothing.
150  },
151  ignoreAllLogs: (value) => {
152    // Do nothing.
153  },
154  install: () => {
155    // Do nothing.
156  },
157  uninstall: () => {
158    // Do nothing.
159  },
160}));
161
162try {
163  jest.mock('expo-modules-core', () => {
164    const ExpoModulesCore = jest.requireActual('expo-modules-core');
165    const uuid = jest.requireActual('expo-modules-core/build/uuid/uuid.web');
166
167    const { NativeModulesProxy } = ExpoModulesCore;
168
169    // After the NativeModules mock is set up, we can mock NativeModuleProxy's functions that call
170    // into the native proxy module. We're not really interested in checking whether the underlying
171    // method is called, just that the proxy method is called, since we have unit tests for the
172    // adapter and believe it works correctly.
173    //
174    // NOTE: The adapter validates the number of arguments, which we don't do in the mocked functions.
175    // This means the mock functions will not throw validation errors the way they would in an app.
176
177    // Mock the `uuid` object with the implementation for web.
178    ExpoModulesCore.uuid.v4 = uuid.default.v4;
179    ExpoModulesCore.uuid.v5 = uuid.default.v5;
180
181    for (const moduleName of Object.keys(NativeModulesProxy)) {
182      const nativeModule = NativeModulesProxy[moduleName];
183      for (const propertyName of Object.keys(nativeModule)) {
184        if (typeof nativeModule[propertyName] === 'function') {
185          nativeModule[propertyName] = jest.fn(async () => {});
186        }
187      }
188    }
189
190    return ExpoModulesCore;
191  });
192} catch (error) {
193  // Allow this module to be optional for bare-workflow
194  if (error.code !== 'MODULE_NOT_FOUND') {
195    throw error;
196  }
197}
198