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