I'm trying to access the MetaDetails component in the test case but unable to do so since its wrapped with the Provider. The goal is to access the PrintLabelButton component on click of which I need to mock 'handlePrintLabelButtonClick' function.
Utils.js
export const handlePrintLabelButtonClick = (
e,
rmaNumber,
labelUrl,
getReturnLabel
) => {
const rmaList = [];
e.preventDefault();
if (!labelUrl) {
const container = { rmaNo: "" };
container.rmaNo = rmaNumber;
rmaList.push(container);
getReturnLabel(rmaList);
} else {
window.open(labelUrl, "_blank");
}
};
Page.js
import {
filterReturnLabelResponse,
handlePrintLabelButtonClick
} from "../../utils/common-util";
import { PrintLabelButton } from "./printLabel";
Const MetaDetails = () => {
//a lot of code
{showPrintLabelButton && (
<PrintLabelButton
onClickHandle={e =>
handlePrintLabelButtonClick(
e,
rmaNumber,
labelUrl,
getReturnLabel
)
}
url={labelUrl}
target="_blank"
type="printLabel"
text={intl.formatMessage(messages.printLabelText)}
/>
)}
// again a lot of code
}
const mapStateToProps = state => ({
// some code
});
const mapDispatchToProps = dispatch => ({
// some code
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(injectIntl(MetaDetails));
PrintLabelButton.js
export const PrintLabelButton = ({
target,
url,
type,
text,
onClickHandle
}: PrintLabelButtonProps) => {
return (
<ButtonWrapper>
<AnchorWrapper
href={url}
target={target}
type={type}
>
<DefaultButton
tabIndex="0"
onClick={onClickHandle}
data-test="print-label-button"
>
{text}
</DefaultButton>
</AnchorWrapper>
</ButtonWrapper>
);
};
What I've tried so far
MetaDetails.test.js
test("Returns Component Whats next button click", () => {
const wrapper = mount(
<Provider store={store}>
<MetaDetails/>
</Provider>
);
const middle = wrapper.find(MetaDetails);
const component = wrapper.find(`[data-test="print-label-button"]`).hostNodes();
component.simulate("click");
expect(mockSetModal).toHaveBeenCalled();
});
Can someone please tell me how can I access nodes when your component is surrounded by the Provider.
Note: I am using react/#emotion
Related
I have a React app which utilizes the context hook. The app functions properly but I am having difficulty writing passing tests.
My context looks like
import React, { createContext } from 'react';
const DataContext = createContext();
export const DataProvider = (props) => {
const [personSuccessAlert, setPersonSuccessAlert] = React.useState(false);
return (
<DataContext.Provider
value={{
personSuccessAlert,
setPersonSuccessAlert,
}}>
{props.children}
</DataContext.Provider>
);
};
export const withContext = (Component) => (props) => (
<DataContext.Consumer>
{(globalState) => <Component {...globalState} {...props} />}
</DataContext.Consumer>
);
The app uses this context in a useEffect hook
import React, { useEffect } from 'react';
import { Alert } from '../../../components/alert';
const PersonRecord = ({
match: {
params: { id },
},
setPersonSuccessAlert,
personSuccessAlert,
}) => {
const closeAlert = () => {
setTimeout(() => {
setPersonSuccessAlert(false);
}, 3000);
};
useEffect(() => {
closeAlert();
}, [location]);
return (
<>
<Alert
open={personSuccessAlert}
/>
</>
);
};
export default withContext(PersonRecord);
This all works as expected. When I run my tests I know I need to import the DataProvider and wrap the component but I keep getting an error.
test('useeffect', async () => {
const history = createMemoryHistory();
history.push('/people');
const setPersonSuccessAlert = jest.fn();
const { getByTestId, getByText } = render(
<DataProvider value={{ setPersonSuccessAlert }}>
<MockedProvider mocks={mocksPerson} addTypename={false}>
<Router history={history}>
<PersonRecord match={{ params: { id: '123' } }} />
</Router>
</MockedProvider>
</DataProvider>,
);
const alert = getByTestId('styled-alert');
await act(async () => new Promise((resolve) => setTimeout(resolve, 4000)));
});
There are a few different errors I get depending on how I change things up but the most common is
[TypeError: setPersonSuccessAlert is not a function]
I think my context is setup slightly different than others which is why I am having trouble using other methods found on here.
I have a React HOC that hides a flyover/popup/dropdown, whenever I click outside the referenced component. It works perfectly fine when using local state. My HOC looks like this:
export default function withClickOutside(WrappedComponent) {
const Component = props => {
const [open, setOpen] = useState(false);
const ref = useRef();
useEffect(() => {
const handleClickOutside = event => {
if (ref?.current && !ref.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => setOpen(false);
}, [ref]);
return <WrappedComponent open={open} setOpen={setOpen} ref={ref} {...props} />;
};
return Component;
}
When in use I just wrap up the required component with the HOC function
const TestComponent = () => {
const ref = useRef();
return <Wrapper ref={ref} />;
}
export default withClickOutside(TestComponent);
But I have some flyover containers that are managed from Redux when they are shown, or when they are hidden. When the flyover is shown, I want to have the same behavior, by clicking outside the referenced component to hide it right away. Here's a example of a flyover:
const { leftFlyoverOpen } = useSelector(({ toggles }) => toggles);
return (
<div>
<Wrapper>
<LeftFlyoverToggle
onClick={() => dispatch({ type: 'LEFT_FLYOVER_OPEN' })}
>
...
</Wrapper>
{leftFlyoverOpen && <LeftFlyover />}
{rightFlyoverOpen && <RightFlyover />}
</div>
);
Flyover component looks pretty straightforward:
const LefFlyover = () => {
return <div>...</div>;
};
export default LefFlyover;
Question: How can I modify the above HOC to handle Redux based flyovers/popup/dropdown?
Ideally I would like to handle both ways in one HOC, but it's fine if the examples will be only for Redux solution
You have a few options here. Personally, I don't like to use HOC's anymore. Especially in combination with functional components.
One possible solution would be to create a generic useOnClickOutside hook which accepts a callback. This enables you to dispatch an action by using the useDispatch hook inside the component.
export default function useOnClickOutside(callback) {
const [element, setElement] = useState(null);
useEffect(() => {
const handleClickOutside = event => {
if (element && !element.contains(event.target)) {
callback();
}
};
if (element) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [element, callback]);
return setElement;
}
function LeftFlyOver() {
const { leftFlyoverOpen } = useSelector(({ toggles }) => toggles);
const dispatch = useDispatch();
const setElement = useOnClickOutside(() => {
dispatch({ type: 'LEFT_FLYOVER_CLOSE' });
});
return (
<Dialog open={leftFlyoverOpen} ref={ref => setElement(ref)}>
...
</Dialog>
)
}
I need to make a click on the button in one component and on this click call a function in the adjacent one. What's the easiest way?
I implemented like this. https://stackblitz.com/edit/react-l5beyi But I think you can do it much easier. React is new to me, and this construction looks strange ...
const App = () => {
const [isAdded, setIsAdded] = useState(false);
function handleClick(status) {
setIsAdded(status)
}
return (
<div>
<ComponentFirst
HandleClick={handleClick}
/>
<ComponentSecond
isAdded={isAdded}
handleCreate={handleClick}
/>
</div>
);
}
const ComponentFirst = ({ HandleClick }) => {
return (
<button
onClick={HandleClick}
>button</button>
)
}
const ComponentSecond = (props) => {
let { isAdded, handleCreate } = props;
const result = () => {
alert('work')
console.log('work')
}
React.useEffect(() => {
if (isAdded) {
result()
handleCreate(false);
}
}, [isAdded, handleCreate]);
return (
<></>
)
}
In your (contrived, I suppose) example the second component doesn't render anything, so it doesn't exist. The work should be done by the parent component:
const App = () => {
const handleClick = React.useCallback((status) => {
alert(`work ${status}`);
// or maybe trigger some asynchronous work?
}, []);
return (
<div>
<ComponentFirst handleClick={handleClick} />
</div>
);
};
const ComponentFirst = ({ handleClick }) => {
return <button onClick={() => handleClick("First!")}>button</button>;
};
You can also use a CustomEvent to which any component can listen to.
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
return (
<div>
<ComponentFirst />
<ComponentSecond />
</div>
);
}
const ComponentFirst = () => {
const handleClick = (e) => {
// Create the event.
const event = new CustomEvent("myCustomEventName", {
detail: "Some information"
});
// target can be any Element or other EventTarget.
window.dispatchEvent(event);
};
return <button onClick={handleClick}>button</button>;
};
const ComponentSecond = (props) => {
function eventHandler(e) {
console.log("Dispatched Detail: " + e.detail);
}
//Listen for this event on the window object
useEffect(() => {
window.addEventListener("myCustomEventName", eventHandler);
return () => {
window.removeEventListener("myCustomEventName", eventHandler);
};
});
return <></>;
};
I really like react context, but I think it's missing something (or maybe I don't know how to do it)
Say I have a list of todos and it's corresponding provider as
const Home = () => (
<div className="container">
<TodosProvider>
<TodosList />
</TodosProvider>
</div>
)
const TodosList = () => {
const { todos } = useTodos();
return (
<>
{todos.map((todo, idx) => (
<SingleTodo />
))}
</>
)
}
And in another file
import { createContext, useContext, useState } from "react";
const TodosContext = createContext({});
export const TodosProvider = ({ children }) => {
const [todos, setTodos] = useState([{ text: 'a' }, { text: 'b' }, { text: 'c' }])
return (
<TodosContext.Provider value={{ todos }}>
{children}
</TodosContext.Provider>
)
}
export const useTodos = () => {
const todos = useContext(TodosContext)
return todos
}
How can I update a single todo inside the SingleTodo without:
1) Passing the map idx as a property to the SingleTodo and then from SingleTodo call a method of the TodosList provider passing the idx as a parameter
2) Giving an artificial id property to the todo. And then in TodosProvider update the todo that matches with that id.
The reasons for those restrictions are that:
1) Passing down the position of the todo in the rendering as a prop, invalidates the benefits of using context, which is to not have to do prop drilling
2) I don't think it's good to pollute the model with an artificial id just for state management.
I'd like to be able to create a SingleTodoContext and instantiate a SingleTodoProvider in each iteration of the loop
const TodosList = () => {
const { todos } = useTodos();
return (
<>
{todos.map((todo, idx) => (
<SingleTodoProvider key={idx} loadFrom={todo}>
<SingleTodo />
</SingleTodoProvider>
))}
</>
)
}
But that doesn't work because the provider would then need to store the loadFrom property as a state, and that would break the sync between the list todo, and the single todo.
So, how do I update a single item inside a list without prop drilling the position of the item in the list? I don't want to use Redux
You can pass methods for updating the values in context as part of your context. Here is an example based on your code (sort of all crammed together):
import React from "react";
import "./styles.css";
import { createContext, useContext, useState } from "react";
const TodosContext = createContext({});
export const TodosProvider = ({ children }) => {
const [todos, setTodos] = useState([
{ text: "a" },
{ text: "b" },
{ text: "c" }
]);
const selectTodo = (todo, idx) => {
console.log(
"do something with the todo here and then call setTodos, or something else?",
todo.text,
idx
);
// setTodos(prev => /* Do something here to update the list */)
};
return (
<TodosContext.Provider value={{ selectTodo, todos }}>
{children}
</TodosContext.Provider>
);
};
export const useTodos = () => {
const todos = useContext(TodosContext);
return todos;
};
const Home = () => (
<div className="container">
<TodosProvider>
<TodosList />
</TodosProvider>
</div>
);
const SingleTodo = ({ todo, onClick }) => (
<div>
{todo.text} <button onClick={() => onClick(todo)}>Click Me!</button>
</div>
);
const TodosList = () => {
const { selectTodo, todos } = useTodos();
return todos.map((todo, idx) => (
<SingleTodo onClick={todo => selectTodo(todo, idx)} todo={todo} key={idx} />
));
};
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Home />
</div>
);
}
Hope that helps!
I want make higher-order component / HOC wrapped with redux
I tried something like that :
export const Button = connect(mapStateToProps,mapDispatchToProps)(
() => {
return(
<Link
to="#"
className="btn-enter"
onClick={() => this.props.handleShare()}>
Undang Teman
</Link>
);
});
const mapStateToProps = (state) => {
}
const mapDispatchToProps = (dispatch) => {
return {
handleShare: () => dispatch({type: ActionType.HANDLE_SHARE_MODAL})
}
}
But when i run this code i got message error :
ReferenceError: Cannot access 'mapStateToProps' before initialization
Also if you don't need mapStateToProps you can leave it as null
// Better to declare component above and then pass it to connect HOC
const ButtonComponent = (props) => {
return(
<Link
to="#"
className="btn-enter"
// ButtonComponent is a functional component
// and to you props you get them from params
onClick={() => props.handleShare()}>
Undang Teman
</Link>
);
}
const mapDispatchToProps = (dispatch) => {
return {
handleShare: () => dispatch({type: ActionType.HANDLE_SHARE_MODAL})
}
}
export const Button = connect(null, mapDispatchToProps)(ButtonComponent);
as it says you should first initialize function to access it later so it must be
const mapStateToProps = (state) => {
}
const mapDispatchToProps = (dispatch) => {
return {
handleShare: () => dispatch({type: ActionType.HANDLE_SHARE_MODAL})
}
}
export const Button = connect(mapStateToProps,mapDispatchToProps)(
() => {
return(
<Link
to="#"
className="btn-enter"
onClick={() => this.props.handleShare()}>
Undang Teman
</Link>
);
});