xref: /expo/apps/jest-expo-mock-generator/App.js (revision d4de237f)
1import mux from '@expo/mux';
2import { setStringAsync } from 'expo-clipboard';
3import React from 'react';
4import { Button, NativeModules, StyleSheet, Text, View } from 'react-native';
5
6// A workaround for `TypeError: Cannot read property 'now' of undefined` error thrown from reanimated code.
7global.performance = {
8  now: () => 0,
9};
10
11const { ExpoNativeModuleIntrospection } = NativeModules;
12
13if (!ExpoNativeModuleIntrospection) {
14  console.warn(
15    'Looks like there is no `ExpoNativeModuleIntrospection` module. Please make sure you are running this app on iOS.'
16  );
17}
18
19const keysOrder = ['type', 'functionType', 'name', 'argumentsCount', 'key'];
20
21function isNumeric(str) {
22  if (typeof str !== 'string') {
23    return false; // we only process strings!
24  }
25  return (
26    !isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
27    !isNaN(parseFloat(str))
28  ); // ...and ensure strings of whitespace fail
29}
30
31const replacer = (_key, value) => {
32  if (value instanceof Object && !(value instanceof Array)) {
33    return Object.keys(value)
34      .sort(function (a, b) {
35        if (keysOrder.indexOf(a) !== -1 || keysOrder.indexOf(b) !== -1) {
36          return (
37            (keysOrder.includes(a) ? keysOrder.indexOf(a) : Infinity) -
38            (keysOrder.includes(b) ? keysOrder.indexOf(b) : Infinity)
39          );
40        } else {
41          return a.localeCompare(b);
42        }
43      })
44      .reduce((sorted, key) => {
45        sorted[key] = value[key];
46        return sorted;
47      }, {});
48  }
49  if (value instanceof Array) {
50    // sorts by numeric keys eg. { name: 'isAvailableAsync', argumentsCount: 0, key: 0 },
51    if (value?.[0]?.key && isNumeric(value?.[0]?.key)) {
52      return value.sort((a, b) => Number(a?.key) > Number(b?.key));
53    }
54    // sorts by string keys  eg. { name: 'getNetworkStateAsync', argumentsCount: 0, key: 'getNetworkStateAsync' },
55    if (value?.[0]?.key) {
56      return value.sort((a, b) => a?.key?.localeCompare?.(b?.key));
57    }
58    // sort other arrays
59    return value?.sort((a, b) => a?.localeCompare?.(b)) ?? value;
60  }
61  return value;
62};
63
64export default class App extends React.Component {
65  state = {};
66
67  async componentDidMount() {
68    const moduleSpecs = await _getExpoModuleSpecsAsync();
69    const code = `module.exports = ${JSON.stringify(moduleSpecs, replacer)};`;
70    await setStringAsync(code);
71    this.setState({ moduleSpecs: code });
72    const message = `
73
74------------------------------COPY THE TEXT BELOW------------------------------
75
76${code}
77
78------------------------------END OF TEXT TO COPY------------------------------
79
80THE TEXT WAS ALSO COPIED TO YOUR CLIPBOARD
81
82`;
83    console.log(message);
84  }
85
86  render() {
87    return (
88      <View style={styles.container}>
89        <Text style={{ fontWeight: '700' }}>
90          Your new jest mocks should now be:{'\n'}- In your clipboard{'\n'}- In your development
91          console.{'\n\n'}
92          Copy either one of the <Text style={{ backgroundColor: '#eee' }}>
93            module.exports
94          </Text>{' '}
95          line into <Text style={{ backgroundColor: '#eee' }}>jest-expo/src/expoModules.js</Text>{' '}
96          and format it nicely with prettier.
97        </Text>
98        <Button onPress={() => setStringAsync(this.state.moduleSpecs)} title="Copy to clipboard" />
99      </View>
100    );
101  }
102}
103
104async function _getExpoModuleSpecsAsync() {
105  const whitelist = /^(Expo(?:nent)?|AIR|CTK|Lottie|Reanimated|RN|NativeUnimoduleProxy)(?![a-z])/;
106  const moduleNames = await ExpoNativeModuleIntrospection.getNativeModuleNamesAsync();
107  const expoModuleNames = moduleNames.filter((moduleName) => whitelist.test(moduleName)).sort();
108  const specPromises = {};
109  for (const moduleName of expoModuleNames) {
110    specPromises[moduleName] = _getModuleSpecAsync(moduleName, NativeModules[moduleName]);
111  }
112  return await mux(specPromises);
113}
114
115async function _getModuleSpecAsync(moduleName, module) {
116  if (!module) {
117    return {};
118  }
119
120  const moduleDescription = await ExpoNativeModuleIntrospection.introspectNativeModuleAsync(
121    moduleName
122  );
123  const spec = _addFunctionTypes(_mockify(module), moduleDescription.methods);
124  if (moduleName === 'NativeUnimoduleProxy') {
125    spec.exportedMethods.mock = _sortObject(module.exportedMethods);
126    spec.viewManagersMetadata.mock = module.viewManagersMetadata;
127    spec.modulesConstants.type = 'mock';
128    spec.modulesConstants.mockDefinition = Object.keys(module.modulesConstants)
129      .sort()
130      .reduce(
131        (spec, moduleName) => ({
132          ...spec,
133          [moduleName]: module.modulesConstants[moduleName]
134            ? _mockify(module.modulesConstants[moduleName])
135            : undefined,
136        }),
137        {}
138      );
139  }
140  return spec;
141}
142
143const _mockify = (obj, context) =>
144  Object.keys(obj)
145    .sort()
146    .reduce((spec, key) => {
147      const value = obj[key];
148      const type = Array.isArray(value) ? 'array' : typeof value;
149      const mock = type !== 'function' ? _mockifyValue(value, { context, key }) : undefined;
150      return { ...spec, [key]: { type, mock } };
151    }, {});
152
153const _addFunctionTypes = (spec, methods) =>
154  Object.keys(methods)
155    .sort()
156    .reduce(
157      (spec, methodName) => ({
158        ...spec,
159        [methodName]: {
160          ...spec[methodName],
161          functionType: methods[methodName].type,
162        },
163      }),
164      spec
165    );
166
167const _sortObject = (obj) =>
168  Object.keys(obj)
169    .sort()
170    .reduce(
171      (acc, el) => ({
172        ...acc,
173        [el]: obj[el],
174      }),
175      {}
176    );
177
178function _mockifyValue(value) {
179  // Include only values that generally don't contain sensitive data
180  return value === null || typeof value === 'boolean' || typeof value === 'number'
181    ? value
182    : undefined;
183}
184
185const styles = StyleSheet.create({
186  container: {
187    flex: 1,
188    backgroundColor: '#fff',
189    alignItems: 'center',
190    justifyContent: 'center',
191    padding: 20,
192  },
193});
194