How to test function in private scope? - reactjs

I am not able to assert function selectXHandler, when i try to test it directly. Since it's defined in the private scope. I can't mock or spy on this function because you can't access it
export const CheckboxWrapper = ({
x,
y,
z,
a = [],
}: CheckboxWrapperProps) => {
const State = useContext(Context);
const [state, dispatch] = State;
const { Items } = state;
const selectXHandler = () => {
const payload = {
x,
a,
};
dispatch({ type: action, payload });
};
if (y) {
return (
<div className="mt5">
<Checkbox
checked={selectedItems[x] !== undefined}
label={z}
onChange={selectXHandler}
/>
</div>
);
}
return null;
};
const selectXHandler = jest.fn();
await fireEvent.click(checkbox);
expect(checkbox).toBeChecked();
expect(selectXHandler).toHaveBeenCalledTimes(1);
I am getting following error:
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0

As it has discussed in comments above, it's bad move to test some internals. Imagine that's possible, we renamed that internal function or inline it or split into two. Will our test that tests it fail? Definitely. Will our app be broken? Certainly not.
We should be bound to public interface. For component public interface is:
imports(like input values)
props(also input value)
render result(output value)
contexts if any used(also let's treat as input value)
So I see test in next way:
for some input props
we simulate click on Checkbox
and verify that dispatch from context has been called with some desired argument.
I don't know RTL enough so my sample will be about Enzyme. But I'm sure it will be easy to translate it into appropriate RTL selector queries.
import Context from '../where/its/placed/someContext.js';
it('dispatch is called with A on checkbox is clicked', () => {
const dispatch = jest.fn();
const state = {}; // or some initial state your test requires
const ourComponent = mount(<Content.Provider value={{ dispatch, state }}>
<CheckboxWrapper {...somePropsYouNeed} /></Content.Provider>);
ourComponent.find({label: 'some-label'}).simulate('change');
expect(dispatch).toHaveBeenCalledWith({ type: 'someType', payload: 'somePayload' });
expect(dispatch).toHaveBeenCalledTimes(1);
});

Related

Typescript complaining when calling a method from React useState initialized with an empty object

In the main App component I have an event handler that passes an onSubmit and onClose callback to the onOpenDialog function
App.ts
const App = () => {
const { onOpenDialog, onCloseDialog } = useDialog()
const onOpenModal = () => {
// Open the Dialog and pass callbacks
onOpenDialog({
onSubmit: (data: any) => {
// ... CRUD operation
onCloseDialog()
},
onCancel: onCloseDialog
})
return <button onClick={onOpenModal}>Open</button>
}
The onOpenDialog function comes from the useDialog hook which stores the callback in the Dialog state.
This state is initialized with an empty object ({}). It is populated when the modal is open and reseted to its initial value ({}) when the modal is closed.
useDialog.ts
const useDialog = () => {
const [open, setOpen] = useState<boolean>(false)
const [dialogState, setDialogState] = useState<object>({})
const onOpenDialog = (config?: object): void => {
setOpen(true)
if (isObject(config) && !isEmpty(config)) {
setDialogState(config!)
}
}
const onCloseDialog = (): void => {
setOpen(false)
if (!isEmpty(dialogState)) {
setDialogState({})
}
}
}
Finally, I have a hook for my modal form component that exports two handlers (onSubmit and onCancel) which will be called in the modal form component
useModalForm.ts
const useModalForm = () => {
const { dialogState } = useDialog()
const onSubmit = (data: any) => {
dialogState.onSubmit(data) // Property 'onSubmit' does not exist on type 'object'
}
return {
onSubmit: onSubmit,
onCancel: dialogState.onCancel,
}
}
I can't call dialogState.onSubmit(data) because typescript complains that onSubmit doesn't exist in type 'object' (which I understand)
How can I - in Typescript - set the initial/closed state to {} and populate it with whatever callback or data I pass when the modal is open.
I tried to type useState with an interface of the signature of the expected state but when I close the modal, I can't reset the dialogConfig to {} without Typescript complaining.
I had the same problem and tried the same solutions.
Here's what I found in the React+TypeScript Cheatsheets:
Approach 1
Create a type for the expected state then "explicitly declare the type, and use a union type".
type IDialogState = { /* Describe the shape of your state */ };
const [dialogState, setDialogState] = React.useState<IDialogState | null>(null);
Approach 2
"You can also use type assertions if a state is initialized soon after setup and always has a value after:"
type IDialogState = { /* Describe the shape of your state */ };
const [dialogState, setDialogState] = React.useState<IDialogState>({} as IDialogState);
"This temporarily 'lies' to the TypeScript compiler that {} is of type IDialogState. You should follow up by setting the user state — if you don't, the rest of your code may rely on the fact that user is of type IDialogState and that may lead to runtime errors."
Source: React+TypeScript Cheatsheets
(variable names adapted)
Related:
as in React Hooks to give the initial value an empty object
https://jsramblings.com/how-to-use-usestate-hook-in-react-with-typescript/
https://www.carlrippon.com/typed-usestate-with-typescript/

Limit renders of component that uses useContext via a useFooController/useFooHook

I'm using custom hooks for a component, and the custom hook uses a custom context. Consider
/* assume FooContext has { state: FooState, dispatch: () => any } */
const useFoo = () => {
const { state, dispatch } = useContext(FooContextContext)
return {apiCallable : () => apiCall(state) }
}
const Foo = () => {
const { apiCallable } = useFoo()
return (
<Button onClick={apiCallable}/>
)
}
Lots of components will be making changes to FooState from other components (form inputs, etc.). It looks to me like Foo uses useFoo, which uses state from FooStateContext. Does this mean every change to FooContext will re-render the Foo component? It only needs to make use of state when someone clicks the button but never otherwise. Seems wasteful.
I was thinking useCallback is specifically for this, so I am thinking return {apiCallable : useCallback(() => apiCall(state)) } but then I need to add [state] as a second param of useCallback. Then that means the callback will be re-rendered whenever state updates, so I'm back at the same issue, right?
This is my first time doing custom hooks like this. Having real difficulty understanding useCallback. How do I accomplish what I want?
Edit Put another way, I have lots of components that will dispatch small changes to deeply nested properties of this state, but this particular component must send the entire state object via a RESTful API, but otherwise will never use the state. It's irrelevant for rendering this component completely. I want to make it so this component never renders even when I'm making changes constantly to the state via keypresses on inputs (for example).
Since you provided Typescript types in your question, I will use them in my response.
Way One: Split Your Context
Given a context of the following type:
type ItemContext = {
items: Item[];
addItem: (item: Item) => void;
removeItem: (index: number) => void;
}
You could split the context into two separate contexts with the following types:
type ItemContext = Item[];
type ItemActionContext = {
addItem: (item: Item) => void;
removeItem: (index: number) => void;
}
The providing component would then handle the interaction between these two contexts:
const ItemContextProvider = () => {
const [items, setItems] = useState([]);
const actions = useMemo(() => {
return {
addItem: (item: Item) => {
setItems(currentItems => [...currentItems, item]);
},
removeItem: (index: number) => {
setItems(currentItems => currentItems.filter((item, i) => index === i));
}
};
}, [setItems]);
return (
<ItemActionContext.Provider value={actions}>
<ItemContext.Provider value={items}>
{children}
</ItemContext.Provider>
</ItemActionContext.Provider>
)
};
This would allow you to get access to two different contexts that are part of one larger combined context.
The base ItemContext would update as items are added and removed causing rerenders for anything that was consuming it.
The assoicated ItemActionContext would never update (setState functions do not change for their lifetime) and would never directly cause a rerender for a consuming component.
Way Two: Some Version of an Subscription Based Value
If you make the value of your context never change (mutate instead of replace, HAS THE WORLD GONE CRAZY?!) you can set up a simple object that holds the data you need access to and minimises rerenders, kind of like a poor mans Redux (maybe it's just time to use Redux?).
If you make a class similar to the following:
type Subscription<T> = (val: T) => void;
type Unsubscribe = () => void;
class SubscribableValue<T> {
private subscriptions: Subscription<T>[] = [];
private value: T;
constructor(val: T) {
this.value = val;
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.subscribe = this.subscribe.bind(this);
}
public get(): T {
return this._val;
}
public set(val: T) {
if (this.value !== val) {
this.value = val;
this.subscriptions.forEach(s => {
s(val)
});
}
}
public subscribe(subscription: Subscription<T>): Unsubscriber {
this.subscriptions.push(subscription);
return () => {
this.subscriptions = this.subscriptions.filter(s => s !== subscription);
};
}
}
A context of the following type could then be created:
type ItemContext = SubscribableValue<Item[]>;
The providing component would look something similar to:
const ItemContextProvider = () => {
const subscribableValue = useMemo(() => new SubscribableValue<Item[]>([]), []);
return (
<ItemContext.Provider value={subscribableValue}>
{children}
</ItemContext.Provider>
)
};
You could then use some a custom hooks to access the value as needed:
// Get access to actions to add or remove an item.
const useItemContextActions = () => {
const subscribableValue = useContext(ItemContext);
const addItem = (item: Item) => subscribableValue.set([...subscribableValue.get(), item]);
const removeItem = (index: number) => subscribableValue.set(subscribableValue.get().filter((item, i) => i === index));
return {
addItem,
removeItem
}
}
type Selector = (items: Item[]) => any;
// get access to data stored in the subscribable value.
// can provide a selector which will check if the value has change each "set"
// action before updating the state.
const useItemContextValue = (selector: Selector) => {
const subscribableValue = useContext(ItemContext);
const selectorRef = useRef(selector ?? (items: Item[]) => items)
const [value, setValue] = useState(selectorRef.current(subscribableValue.get()));
const useEffect(() => {
const unsubscribe = subscribableValue.subscribe(items => {
const newValue = selectorRef.current(items);
if (newValue !== value) {
setValue(newValue);
}
})
return () => {
unsubscribe();
};
}, [value, selectorRef, setValue]);
return value;
}
This would allow you to reduce rerenders using selector functions (like an extremely basic version of React Redux's useSelector) as the subscribable value (root object) would never change reference for its lifetime.
The downside of this is that you have to manage the subscriptions and always use the set function to update the held value to ensure that the subscriptions will be notified.
Conclusion:
There are probably a number of other ways that different people would attack this problem and you will have to find one that suits your exact issue.
There are third party libraries (like Redux) that could also help you with this if your context / state requirements have a larger scope.
Does this mean every change to FooContext will re-render the Foo component?
Currently (v17), there is no bailout for Context API. Check my another answer for examples. So yes, it will always rerender on context change.
It only needs to make use of state when someone clicks the button but never otherwise. Seems wasteful.
Can be fixed by splitting context providers, see the same answer above for explanation.

Is there a way to pass React state into WebSocket.onmessage?

In TypeScript I am trying to read the value of a stateful variable (created with React.useState) from within a WebSocket's onmessage function. The value is only ever read as what it was at the time of onmessage definition (the initial state, {state: 1}).
Foo is created like a React component (<Foo />). Bar is called only once, but the state (myState) can be updated many times. Why isn't WebSocket.onmessage() using the reference passed to it?
I tried changing the type of propState from MyStatefulObject to &MyStatefulObject and executing the Bar function code directly, but it didn't work and Bar is actually quite large, so that is not ideal.
import { useEffect, useState } from 'react';
interface MyStatefulObject {state: number}
interface BarProps {propState: &MyStatefulObject}
const Foo = () => {
const [myState, setMyState] = useState<MyStatefulObject>({state: 1}),
[wsInitialized, setWSInitialized] = useState<boolean>(false);;
useEffect(() => {
if (!wsInitialized) {
Bar({propState: myState});
setWSInitialized(true);
}
})
return (
<TextField
onChange={(e: ChangeEvent<HTMLInputElement>) => {
setMyState({state: +(e.target.value)});
console.log(myState); // prints user input, confirming state change
}}
/>
)
}
const Bar = (p: BarProps) => {
const ws = WebSocket('url');
ws.onopen = () => {
ws.send(/* Initialization Message */);
};
ws.onmessage = (e: MessageEvent) => {
console.log(p.propState.state); // always prints 1
};
}

Test useRef onError Fn, with React-Testing-Library and Jest

I have this simple fallbackImage Component:
export interface ImageProps {
srcImage: string;
classNames?: string;
fallbackImage?: FallbackImages;
}
const Image = ({
srcImage,
classNames,
fallbackImage = FallbackImages.FALLBACK
}: ImageProps) => {
const imgToSourceFrom = srcImage;
const imgToFallbackTo = fallbackImage;
const imageRef = useRef(null);
const whenImageIsMissing = () => {
imageRef.current.src = imgToFallbackTo;
imageRef.current.onerror = () => {};
};
return (
<img ref={imageRef} src={imgToSourceFrom} className={classNames} onError={whenImageIsMissing} />
);
};
export default Image;
It works perfectly. I have test running for it with Jest and React-Testing-Library. I have tested all but one scenario. This one:
const whenImageIsMissing = () => {
imageRef.current.src = imgToFallbackTo;
imageRef.current.onerror = () => {}; // This line.
};
This line basically prevents an infinite Loop in case both images are missing
The Problem:
I want to test that my onerror function has been called exactly one time. Which I am really stuck on how to do it. Here is the test...
const { container } = render(<Image srcImage={undefined} fallbackImage={undefined} />);
const assertion = container.querySelector('img').onerror;
fireEvent.error(container.firstElementChild);
console.log(container.firstElementChild);
expect(container.firstElementChild.ref.current.onerror).toHaveBeenCalledTimes(1);
// This though has no reference to a real value. Is an example of what I want to get at.
The Question:
How to access the ref callback function and check how many times has my function been called?
Any ideas on this. I am at a loss, I tried mocking refs, I tried mocking and spying on the component. I tried using act and async/await, in case it was called after. I really need some help on this..
You should check if your function is called or not, that's called testing implementation details, rather you should check if your img element have correct src.
Even you should add some alt and user getByAltText to select image element
const { getByAltText } = render(<Image srcImage={undefined} fallbackImage={undefined} />);
const imageElement = getByAltText('Image Alt');
fireEvent.error(imageElement);
expect(imageElement.src).toEqual(imgToFallbackTo);
You have 2 options:
Add a callback to your props that will be called when whenImageIsMissing is called:
export interface ImageProps {
srcImage: string;
classNames?: string;
fallbackImage?: FallbackImages;
onImageMissing?:();
}
const Image = ({
srcImage,
classNames,
onImageMissing,
fallbackImage = FallbackImages.FALLBACK
}: ImageProps) => {
const imgToSourceFrom = srcImage;
const imgToFallbackTo = fallbackImage;
const imageRef = useRef(null);
const whenImageIsMissing = () => {
imageRef.current.src = imgToFallbackTo;
imageRef.current.onerror = () => {};
if (onImageMissing) onImageMissing();
};
return (
<img ref={imageRef} src={imgToSourceFrom} className={classNames} onError={whenImageIsMissing} />
);
};
and then insert jest.fn in your test and check how many times it was called.
The other option is to take the implementation of whenImageIsMissing and put it inside image.util file and then use jest.spy to get number of calls. Since you are using a function component there is no way to access this function directly.
Hope this helps.

Jest/Enzyme Shallow testing RFC - not firing jest.fn()

I'm trying to test the onChange prop (and the value) of an input on an RFC. On the tests, trying to simulate the event doesn't fire the jest mock function.
The actual component is connected (with redux) but I'm exporting it also as an unconnected component so I can do a shallow unit test. I'm also using some react-spring hooks for animation.
I've also tried to mount instead of shallow the component but I still get the same problem.
MY Component
export const UnconnectedSearchInput: React.FC<INT.IInputProps> = ({ scrolled, getUserInputRequest }): JSX.Element => {
const [change, setChange] = useState<string>('')
const handleChange = (e: InputVal): void => {
setChange(e.target.value)
}
const handleKeyUp = (): void => {
getUserInputRequest(change)
}
return (
<animated.div
className="search-input"
data-test="component-search-input"
style={animateInputContainer}>
<animated.input
type="text"
name="search"
className="search-input__inp"
data-test="search-input"
style={animateInput}
onChange={handleChange}
onKeyUp={handleKeyUp}
value={change}
/>
</animated.div>
)
}
export default connect(null, { getUserInputRequest })(UnconnectedSearchInput);
My Tests
Here you can see the test that is failing. Commented out code is other things that I-ve tried so far without any luck.
describe('test input and dispatch action', () => {
let changeValueMock
let wrapper
const userInput = 'matrix'
beforeEach(() => {
changeValueMock = jest.fn()
const props = {
handleChange: changeValueMock
}
wrapper = shallow(<UnconnectedSearchInput {...props} />).dive()
// wrapper = mount(<UnconnectedSearchInput {...props} />)
})
test('should update input value', () => {
const input = findByTestAttr(wrapper, 'search-input').dive()
// const component = findByTestAttr(wrapper, 'search-input').last()
expect(input.name()).toBe('input')
expect(changeValueMock).not.toHaveBeenCalled()
input.props().onChange({ target: { value: userInput } }) // not geting called
// input.simulate('change', { target: { value: userInput } })
// used with mount
// act(() => {
// input.props().onChange({ target: { value: userInput } })
// })
// wrapper.update()
expect(changeValueMock).toBeCalledTimes(1)
// expect(input.prop('value')).toBe(userInput);
})
})
Test Error
Nothing too special here.
expect(jest.fn()).toBeCalledTimes(1)
Expected mock function to have been called one time, but it was called zero times.
71 | // wrapper.update()
72 |
> 73 | expect(changeValueMock).toBeCalledTimes(1)
Any help would be greatly appreciated since it's been 2 days now and I cn't figure this out.
you don't have to interact with component internals; instead better use public interface: props and render result
test('should update input value', () => {
expect(findByTestAttr(wrapper, 'search-input').dive().props().value).toEqual('');
findByTestAttr(wrapper, 'search-input').dive().props().onChange({ target: {value: '_test_'} });
expect(findByTestAttr(wrapper, 'search-input').dive().props().value).toEqual('_test_');
}
See you don't need to check if some internal method has been called, what's its name or argument. If you get what you need - and you require to have <input> with some expected value - it does not matter how it happened.
But if function is passed from the outside(through props) you will definitely want to verify if it's called at some expected case
test('should call getUserInputRequest prop on keyUp event', () => {
const getUserInputRequest = jest.fn();
const mockedEvent = { target: { key: 'A' } };
const = wrapper = shallow(<UnconnectedSearchInput getUserInputRequest={getUserInputRequest } />).dive()
findByTestAttr(wrapper, 'search-input').dive().props().onKeyUp(mockedEvent)
expect(getUserInputRequest).toHaveBeenCalledTimes(1);
expect(getUserInputRequest).toHaveBeenCalledWith(mockedEvent);
}
[UPD] seems like caching selector in interm variable like
const input = findByTestAttr(wrapper, 'search-input').dive();
input.props().onChange({ target: {value: '_test_'} });
expect(input.props().value).toEqual('_test_');
does not pass since input refers to stale old object where value does not update.
At enzyme's github I've been answered that it's expected behavior:
This is intended behavior in enzyme v3 - see https://github.com/airbnb/enzyme/blob/master/docs/guides/migration-from-2-to-3.md#calling-props-after-a-state-change.
So yes, exactly - everything must be re-found from the root if anything has changed.

Resources