I am testing this connected component:
export class ExclusiveSelectboxesFormSectionView extends React.Component<Props> {
handleSelection = (field: Field, message: string) => {
this.props.setCard({ message, field });
};
cancelSelection = () => this.props.setCard({ message: null, field: null });
render() {
return (
<View>
{this.props.cancelTitle && (
<CheckBox
title={this.props.cancelTitle}
checked={this.props.card.field == null}
onPress={this.cancelSelection.bind(this)}
checkedIcon="dot-circle-o"
uncheckedIcon="circle-o"
/>
)}
{this.props.fields.map((field, i) => {
const props = {};
props.isSelected = this.props.card.field == field;
props.selectionHandler = this.handleSelection.bind(this);
return <ExclusiveFieldView field={field} key={i} {...props} />;
})}
</View>
);
}
}
const mapState = ({ currentFormReducer }) => {
const card = currentFormReducer.card || { message: null, field: null };
return { card };
};
const mapDispatch = { setCard };
export default connect(mapState, mapDispatch)(ExclusiveSelectboxesFormSectionView);
I'm trying to test the non-connected component using react-native-testing-library. The component works in the app, but this test is failing to find "Second field option 2" in the next-to-last assertion in the test.
// non-connected component
import { ExclusiveSelectboxesFormSectionView } from "../../src/components/ExclusiveSelectboxesFormSectionView";
function createWrapper(customProps) {
let mockCard = { message: null, field: null };
const props = {
fields,
setCard: jest.fn().mockImplementation((card: Types.Card) => {
mockCard = card;
}),
card: mockCard,
...customProps
};
wrapper = render(
<Fragment>
<ExclusiveSelectboxesFormSectionView fields={fields} {...props} />
</Fragment>
);
return wrapper;
}
describe("ExclusiveSelectboxesFormSectionView", () => {
let checkboxes;
beforeEach(() => {
wrapper = createWrapper();
checkboxes = wrapper.getAllByType(CheckBox);
expect(checkboxes.length).toBe(3);
});
fit("shows the value of the currently selected field", async () => {
await fireEvent.press(checkboxes[1]); // show options
await fireEvent.press(wrapper.getByText("Second field option 2")); // select option
const component = wrapper.getByType(ExclusiveSelectboxesFormSectionView);
expect(component.props.setCard).toHaveBeenCalled();
// options should be gone
expect(wrapper.queryByText("Second field option 1")).toBeNull();
// selected option should still be on screen
expect(wrapper.getByText("Second field option 2")).toBeDefined();
expect(checkboxes[1].props.checked).toBe(true);
});
});
I've passed in a card prop and a setCard mock function prop, in place of redux providing these.
The mock setCard function is being called, so I think the problem is that the component is not rerendering with its new props (and a newly set card prop). A log statement in the component's render function confirms this (it only prints once when the test is run).
I imagine there's something basic I'm missing about how I'm rendering the component, or wrapping it, or calling it, or something.
Can anyone spot my problem?
It looks like react-native-testing-library's update function does the trick. But it was a bit tough to figure out.
// refactored from createWrapper
function getWrapperProps() {
return {
fields,
setCard: jest.fn().mockImplementation((card: Types.Card) => {
mockCard = card;
}),
card: mockCard
};
}
function createWrapper(customProps) {
wrapper = render(
<Fragment>
<ExclusiveSelectboxesFormSectionView
{...getWrapperProps()}
{...customProps}
/>
</Fragment>
);
return wrapper;
}
function updateWrapper(customProps) {
wrapper.update(
<Fragment>
<ExclusiveSelectboxesFormSectionView
{...getWrapperProps()}
{...customProps}
/>
</Fragment>
);
checkboxes = wrapper.getAllByType(CheckBox);
}
// call updateWrapper() when you need to get the newly rendered props
it("shows the value of the currently selected field", async () => {
await fireEvent.press(checkboxes[1]);
await fireEvent.press(wrapper.getByText("Second field option 2"));
const component = wrapper.getByType(ExclusiveSelectboxesFormSectionView);
expect(component.props.setCard).toHaveBeenCalled();
updateWrapper();
// options should be gone
expect(wrapper.queryByText("Second field option 1")).toBeNull();
// selected option should still be on screen
expect(wrapper.getByText("Second field option 2")).toBeDefined();
expect(checkboxes[1].props.checked).toBe(true);
});
Now it passes.
I'd love to know of other options. I'm not clear why this had been working with an earlier implementation, although the earlier implementation used state in the component, which I'm sure is basically the answer.
Related
I am using this react library https://github.com/gregberge/loadable-components to load a Component with Ref to access instance values using useImperativeHandle but ref is always null.
Here is my code
import loadable from '#loadable/component';
export default function ParentComponent(props){
const currentPageRef = useRef();
const[currentPage,setCurrentPage]=useState();
const loadPage= (page= 'home') => {
const CurrentPage = loadable(() => import(`./${page}`));
return (
<CurrentPage
ref={currentPageRef}
/>
);
}
useEffect(() => {
console.log(currentPageRef); //This is always logging current(null);
let pageTitle= currentPageRef.current?.getTitle();
let pageSubTitle= currentPageRef.current?.getSubTitle();
console.log(` Page Title=${pageTitle}`); //This is always coming back as null
console.log(`Page SubTitle=${pageSubTitle}`); //This is also always coming back as null
}, [currentPage]);
return (
<div>
<button onClick={() => {
setCurrentPage(loadPage('youtube));
}}>
LoadPage
</button>
</div>
);
}
Where each of the child components contains a useImperativeHandle to expose instance functions but I can't seem to access any of the functions because currentPageRef is always null
Here is an example of one of the child pages that contains the useImperativeHandle implementation
const YouTubePage= React.forwardRef((props,ref)=>{
const [connected, setConnected] = useState(false);
const getTitle = () => {
return connected ? "Your YouTube Channels" : "YouTube";
}
const getSubTitle = () => {
return connected ? "Publishable content is pushed to only connected channels. You may connect or disconnect channel(s) as appropriate" : "Connect a YouTube account to start posting";
}
useImperativeHandle(ref, () => ({ getTitle, getSubTitle }));
return (<div></div>);
});
Any ideas as to why that might be happening?
Thank you
From your code example your aren't actually rendering the component which you set by the state setter:
export default function ParentComponent(props) {
//...
// Render the page
return (
<>
{currentPage}
<div>...</div>
</>
);
}
In Comp1 when i hover mouse on it i want the state to change to true (I'm passing true param to it). Also i want to ensure that by doing so this will not cause the Comp2 component to re-render.
My understanding was that if i do like so useStoreOnHover.setState({ onComp1: true }) it should work but it does not :(
I have also tried with const onComp1Set = useStoreOnHover((s) => s.onComp1Set) but still same :(
The only way i was able to get it working is by const { onComp1Set } = useStoreOnHover() but I'm trying to avoid these type of de-structuring because it also triggers re-renders to other components.
Live example: https://codesandbox.io/s/winter-grass-qxrv8
import create, { GetState, SetState } from "zustand";
type typeStoreOnHover = {
onComp1: boolean;
onComp1Set: (val: boolean) => void;
onComp2: boolean;
};
export const useStoreOnHover = create<typeStoreOnHover>(
(set: SetState<typeStoreOnHover>, get: GetState<typeStoreOnHover>) => {
return {
onComp1: false,
onComp1Set: (val) => set({ onComp1: val }),
onComp2: false
};
}
);
const Comp1 = () => {
const onComp1 = useStoreOnHover.getState().onComp1;
// const onComp1Set = useStoreOnHover((s) => s.onComp1Set);
console.log("Comp 1", onComp1);
return (
<div
onMouseEnter={() => {
// onComp1Set(true);
useStoreOnHover.setState({ onComp1: true });
}}
>
Comp 1 {onComp1 ? "True" : "False"}
</div>
);
};
const Comp2 = () => {
const onComp2 = useStoreOnHover((s) => s.onComp2);
console.log("Comp 2", onComp2);
return <div>Comp 2 </div>;
};
export default function App() {
return (
<>
<Comp1 />
<Comp2 />
</>
);
}
According to zustand documentation, this should be the actual approach,
const onComp1 = useStoreOnHover((s) => s.onComp1);
I have tested it on your CodeSandBox link and it worked.
I am not familiar with the zustand library but this might help you.
I am new to Jestjs and enzyme framework and I am trying to write test cases for a particular react component and I am little stuck.
export class ProductDetailsForm extends Component{
handleMetaDataDefinition = e => {
const { value, name } = e.target;
if (name === "xmlVersion") {
this.checkSpecialCharacters(value);
}
this.setState(prevState => ({
...prevState,
[name]: value
}));
this.props.setProductDetailsFormValue({
...this.props.productDetailsForm,
[name]: value
});
};
checkSpecialCharacters = value => {
if (!value || value.match(/^[a-zA-Z0-9._-]+$/)) {
this.setState(() => ({ error: '' }));
} else {
this.setState(() => ({
error: `Special characters and operators such as !##$%^&*()+{}:;?|\\[]'"= are not allowed`
}));
}
}
render(){
return(
<div>
<MetaDataDefinition
readOnly={false}
metaData={this.state}
handleMetaDataDefinition={this.handleMetaDataDefinition}
validateVersion={this.validateVersion}
/>
</div>
);
}
}
I have started with the test case, but I am stuck and unable to proceed how to work on the function handleMetaDataDefinition for full coverage including the function checkSpecialCharacters. Below is the code that I started to write for ProductDetailsForm.test.js
let wrapper;
beforeEach(() => {
wrapper = shallow(
<ProductDetailForm />
);
});
test("should call handleMetaDataDefinition", ()=> {
wrapper.find('MetaDataDefinition').props('handleMetaDataDefinition');
});
I have used some part of my actual code and not the whole code, as I need help in this specific part only to write test case for handleMetaDataDefinition and checkSpecialCharacters methods.
There're two possible option how to write your tests.
You can trigger validation from your MetaDataDefinition component and pass there needed data.
test("should call handleMetaDataDefinition", ()=> {
const component = wrapper.find('MetaDataDefinition');
fillYourComponentSomehow();
triggerAnEventSomehow();
/*For example component.find('button').simulate('click');
wrapper.update();// We can wait for updating state differently(if needed i'll take a look to doc.)
expect(wrapper.state()).toBe(stateThatYouExpect);
});
Or you can test it as 'black box'
test("should call handleMetaDataDefinition", ()=> {
const component = wrapper.find('MetaDataDefinition');
component.props().handleMetaDataDefinition(objectForMethod)
wrapper.update();
expect(wrapper.state()).toBe(stateThatYouExpect);
});
If you have HOCs around your component you'll need to find this component by class name
wrapper.find('ProductDetailsForm')
UPDATE
You can test it like
let wrapper;
let setProductDetailsFormValue;
beforeEach(() => {
setProductDetailsFormValue = jest.fn();
wrapper = shallow(
<ProductDetailForm setProductDetailsFormValue={setProductDetailsFormValue} />
);
});
test("should call handleMetaDataDefinition", ()=> {
const testObject = { target: {name: 'xmlVersion', value: '!!!123asd!'}, }
const component = wrapper.find('MetaDataDefinition');
component.props().handleMetaDataDefinition(testObject)
wrapper.update();
expect(wrapper.state().error).toBe('Special characters and operators such as !##$%^&*()+{}:;?|\\[]'"= are not allowed');
expect(wrapper.state()[xmlVersion]).toBe('!!!123asd!');
expect(setProductDetailsFormValue).toBeCalledWith({
[xmlVersion]: '!!!123asd!',
...other fields})
});
I have a react native component. I got the error:
Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
Code:
import....
class Register extends Component {
static navigationOptions = {
header: null,
};
async handleSubmit(values, customerCreate) {
const { email, password, firstName, lastName, phone } = values;
const input = { email, password, firstName, lastName, phone };
const customerCreateRes = await customerCreate({ variables: { input } });
const isCustomerCreated = !!customerCreateRes.data.customerCreate.customer.id;
if (isCustomerCreated) {
const isStoredCrediential = await storeCredential(email, password);
if (isStoredCrediential === true) {
// Store in redux
// Go to another screen
console.log('test');
}
}
}
render() {
return (
<Mutation mutation={CREATE_CUSTOMER_ACCOUNT}>
{
(customerCreate, { error, data }) => {
return (
<MainLayout
title="Create Account"
backButton
currentTab="profile"
navigation={this.props.navigation}
>
{ showError }
{ showSuccess }
<RegistrationForm
onSubmit={async (values) => this.handleSubmit(values, customerCreate)}
initialValues={this.props.initialValues}
/>
</MainLayout>
);
}
}
</Mutation>
);
}
}
const mapStateToProps = (state) => {
return {
....
};
};
export default connect(mapStateToProps)(Register);
CREATE_CUSTOMER_ACCOUNT is graphql:
import gql from 'graphql-tag';
export const CREATE_CUSTOMER_ACCOUNT = gql`
mutation customerCreate($input: CustomerCreateInput!) {
customerCreate(input: $input) {
userErrors {
field
message
}
customer {
id
}
}
}
`;
More detail here
Who is using the handleSubmit?
There is a button in the form call the handleSubmit, when press.
is this syntax correct onPress={handleSubmit} ?
const PrimaryButton = ({ label, handleSubmit, disabled }) => {
let buttonStyle = styles.button;
if (!disabled) {
buttonStyle = { ...buttonStyle, ...styles.primaryButton };
}
return (
<Button block primary={!disabled} disabled={disabled} onPress={handleSubmit} style={buttonStyle}>
<Text style={styles.buttonText}>{label}</Text>
</Button>
);
};
export default PrimaryButton;
Update 1:
If I remove customerCreate (coming from graphql), the error disappears. It means the async await is actually correct, but I need the customerCreate
Did you check with following code ?
onSubmit={(values) => this.handleSubmit(values, customerCreate)}
If you are trying to add arguments to a handler in recompose, make sure that you're defining your arguments correctly in the handler.
Also can be you're accidentally calling the onSubmit method in your render method, you probably want to double check how your onSubmit in RegistrationForm component.
Also you might want to try one more thing, moving async handleSubmit(values, customerCreate) { to handleSubmit = async(values, customerCreate) =>;
If this doesn't work, please add up your RegistrationForm component as well.
Bottom line, unless your aren't setting state in render, this will not happen.
It turns out the async await syntax is correct. The full original code (not posted here) contains Toast component react-base. The other developer is able to tell me to remove it and the error is gone. Sometimes it is hard to debug.
I'm sure I'm doing something silly here. I'm trying to write a component which, based on the fetching state of the data, renders one of 3 things.
1) An error message if the fetch has errored.
2) A loading message if the data is fetching.
3) The full child component if the data is fetched.
What's happening now is the fetch succeeds, but the Loading message won't disappear. In the code example below, you can see that I've added a console.log for whether or not the full render should occur. What's perplexing me is that this will eventually log as true, but the Loading message still shows.
This exact pattern seems to work for a sibling component in this app, so I'm confused as to what's missing here... Here's the entire component, it's been anonymized and stripped of styles, but the structure is otherwise identical.
function mapStateToProps(state) {
return {
Data: state.NewDataReducer,
};
}
const mapDispatchToProps = {
fetchNewData: ActionCreators.fetchNewData,
};
const DataError = () => (
<div>
Error fetching new data!
</div>
);
const DataLoading = () => (
<div>
Loading..
</div>
);
class MainContainer extends PureComponent {
static propTypes = {
choice: PropTypes.string,
fetchNewData: PropTypes.func,
Data: ImmutablePropTypes.map.isRequired,
}
state = {
choice: null,
}
componentDidUpdate(nextProps) {
const { choice, fetchNewData, Data } = this.props;
if(!choice) {
return;
}
if(isFetching(Data)) {
return;
}
const newChoiceSelected = choice !== nextProps.choice;
if(newChoiceSelected) {
fetchNewData({choice});
}
}
handleChangeChoice = (choice) => {
this.setState({
choice: { choice }
});
}
render() {
const { choice, Data } = this.props;
const error = hasFetchError(Data);
const loading = !error && !isFetched(Data);
const renderFull = !loading && !error;
console.log(renderFull);
if(!renderFull) {
return (
<div>
Please wait.
</div>
);
}
const { dataBreakdown } = Data.get("dataKey").toJS();
return (
<div>
<ChildComponent
choice={choice}
dataBreakdown={dataBreakdown}
onSetDrillDown={this.handleChangeChoice}
/>
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(MainContainer);
You need to pass your map function the arguments of what it needs to map over from immutable data types. Map creates a new a array and makes a callback function on the items that it iterates through in that array. This is just an example
Data: ImmutabaleProptypes.map((ing, index)=>(
<li key={index}>{Data}</li> // you can write jsx in here.
));