diff --git a/index.js b/index.js index 0d6f44e..2de0d85 100644 --- a/index.js +++ b/index.js @@ -3,13 +3,17 @@ import Tester from './src/Tester'; import TestHookStore from './src/TestHookStore'; import useCavy from './src/useCavy'; import wrap from './src/wrap'; +import cavyCreateElement, { setJSXConfig, Fragment } from './src/cavyCreateElement'; const Cavy = { hook, Tester, TestHookStore, useCavy, - wrap + wrap, + cavyCreateElement, + setJSXConfig, + Fragment }; module.exports = Cavy; diff --git a/sample-app/CavyDirectory/babel.config.js b/sample-app/CavyDirectory/babel.config.js index f842b77..d05c847 100644 --- a/sample-app/CavyDirectory/babel.config.js +++ b/sample-app/CavyDirectory/babel.config.js @@ -1,3 +1,15 @@ module.exports = { presets: ['module:metro-react-native-babel-preset'], + plugins: [ + [ '@wordpress/babel-plugin-import-jsx-pragma', { + scopeVariable: 'cavyCreateElement', + scopeVariableFrag: 'Fragment', + source: 'cavy', + isDefault: false, + } ], + [ '@babel/plugin-transform-react-jsx', { + pragma: 'cavyCreateElement', + pragmaFrag: 'Fragment', + } ], + ] }; diff --git a/sample-app/CavyDirectory/index.js b/sample-app/CavyDirectory/index.js index 747718b..99b7dc1 100644 --- a/sample-app/CavyDirectory/index.js +++ b/sample-app/CavyDirectory/index.js @@ -6,7 +6,7 @@ import React, { Component } from 'react'; import {AppRegistry} from 'react-native'; import {name as appName} from './app.json'; -import { Tester, TestHookStore } from 'cavy'; +import { Tester, TestHookStore, setJSXConfig } from 'cavy'; import EmployeeDirectoryApp from './app/EmployeeDirectoryApp'; @@ -14,6 +14,11 @@ import EmployeeListSpec from './specs/EmployeeListSpec'; const testHookStore = new TestHookStore(); +setJSXConfig({ + testHookStore, + cavyIdPropName: 'yourPropNameForCavyId' +}); + class AppWrapper extends Component { render() { return ( diff --git a/sample-app/CavyDirectory/package.json b/sample-app/CavyDirectory/package.json index 8149373..150e108 100644 --- a/sample-app/CavyDirectory/package.json +++ b/sample-app/CavyDirectory/package.json @@ -15,6 +15,8 @@ "devDependencies": { "@babel/core": "^7.6.2", "@babel/runtime": "^7.6.2", + "@babel/plugin-transform-react-jsx": "^7.9.1", + "@wordpress/babel-plugin-import-jsx-pragma": "^2.5.0", "babel-jest": "^24.9.0", "jest": "^24.9.0", "metro-react-native-babel-preset": "^0.56.0", diff --git a/src/cavyCreateElement.js b/src/cavyCreateElement.js new file mode 100644 index 0000000..762382e --- /dev/null +++ b/src/cavyCreateElement.js @@ -0,0 +1,104 @@ +import React from 'react'; +import cavy from "./generateTestHook"; +import wrap, {isNotReactClass, } from "./wrap"; + +// Public: Drop in replacement for React.createElement that let cavy +// to hook into element creation reducing normal code impact +// directly collecting component refs when the cavyIdProp is set. +// It automatically wraps function components to add forwardRef +// +// If you configure this in you project the code goes from this: +// +// import React from 'react'; +// import { View, Text } from 'react-native'; +// import { useCavy, wrap } from 'cavy'; +// +// export default ({ data }) => { +// const generateTestHook = useCavy(); +// const TestableText = wrap(Text); +// +// return ( +// +// +// {data.title} +// +// +// ) +// }; +// +// to this: +// +// import React from 'react'; +// import { View, Text } from 'react-native'; +// +// export default ({ data }) => {// +// return ( +// +// +// {data.title} +// +// +// ) +// }; +// +// Eve if the configuration is a bit invasive (replace of jsx transpilation) +// it is simple and keeps you src code more free from cavy implementation +// + +// Configuration of jsx element creation +let pragmaConfig = { + testHookStore: null, + cavyIdPropName: 'cavyTestId' +}; + +// Should be called at the startup of you index.test.js +//import { setJSXConfig } from 'cavy-jsx'; +// +// const testHookStore = new TestHookStore(); +// +// setJSXConfig({ +// testHookStore, +// cavyIdPropName: 'yourPropNameForCavyId' +// }); +// +export function setJSXConfig(userConfig) { + if(!userConfig || !userConfig.testHookStore){ + throw new Error(`You must set testHookStore in pragma config`); + } + pragmaConfig = {...pragmaConfig, ...userConfig}; +} + +// +// Simple wrapper of React.createElement that handles element with [cavyIdPropName] +// wrapping function component and collecting its reference in cavy +// +export default function cavyCreateElement(componentType, props, children) { + + // Check it the jsx config has been set + if(!pragmaConfig || !pragmaConfig.testHookStore || pragmaConfig.cavyIdPropName){ + throw new Error('Configure JSX pragma before using it'); + } + // Handle only elements with [cavyIdPropName] set + if (pragmaConfig.cavyIdPropName in props) { + const generateTestHook = cavy(pragmaConfig.testHookStore); + let WrappedType = componentType; + // Auto wraps function components + if (typeof componentType === 'function' && isNotReactClass(componentType)) { + WrappedType = wrap(componentType); + } + let newProps; + if( 'ref' in props){ + // If the ref is already set keep it and ads identifier to cavy + newProps = { ...props, ref: generateTestHook(props[pragmaConfig.cavyIdPropName], props.ref) }; + }else { + // Otherwise just add identifier + newProps = { ...props, ref: generateTestHook(props[pragmaConfig.cavyIdPropName]) }; + } + return React.createElement(WrappedType, newProps, children); + } + + return React.createElement.apply(undefined, arguments); +} + +// JSX replacement must also contains fragment +export const Fragment = React.Fragment; diff --git a/src/wrap.js b/src/wrap.js index d40bf30..bc3333c 100644 --- a/src/wrap.js +++ b/src/wrap.js @@ -96,7 +96,7 @@ export default function wrap(Component) { // checks here. This code is taken from examples in React source code e.g: // // https://github.com/facebook/react/blob/12be8938a5d71ffdc21ee7cf770bf1cb63ae038e/packages/react-refresh/src/ReactFreshRuntime.js#L138 -function isNotReactClass(Component) { +export function isNotReactClass(Component) { return !(Component.prototype && Component.prototype.isReactComponent); }