React Native check context value on click test case - javascript

I have following context
Home.tsx
export const ThemeContext = React.createContext(null)
const Home = () => {
const { width } = Dimensions.get("window")
const [theme, setTheme] = React.useState({
active: 0,
heightOfScrollView: 0,
profileWidth: width * 0.2,
scrolledByTouchingProfile: false
})
const horizontalScrollRef = React.useRef<ScrollView>()
const verticalScrollRef = React.useRef<ScrollView>()
return (
<>
<SafeAreaView style={styles.safeAreaContainer} />
<Header title="Contacts" />
<ThemeContext.Provider value={{ theme, setTheme }}>
In component A, I have a button which changes in the context
const onProfileTouched = (index: number) => {
setTheme({ ...theme, active: index });
};
This leads to an image being active
const ImageCircle = ({ active, uri }: Props) => {
return (
<View
style={
active
? { ...styles.parentView, ...styles.active }
: { ...styles.parentView }
}>
<Image source={uri} width={30} height={30} />
</View>
);
};
Now, I want to write a test case (I haven't written a test case before) that confirms that the state has actually changed or perhaps an active border is added to the image
I added a testId to my button which I used to fire an event
it('changes active on profile clicked', () => {
const { getByTestId } = render(<Home />);
fireEvent.press(getByTestId('HScroll3.button'));
});
Now, I am unsure, how to grab the value of context or change in style so as I can confirm that indeed the component for which the button is pressed is active
I am using import {render, fireEvent} from '#testing-library/react-native' but open to change.

With testing-library you want to check the visual output, not the internal state. This way your tests are much more valuable, because the end user doesn't care if you're using context, state or anything else, they care if the button has the "active" state. So if at some point you'll decide to change your mind and refactor theming completely, your test will still give you value and confidence.
I would suggest to install #testing-library/jest-native, you just have to add this setupFilesAfterEnv": ["#testing-library/jest-native/extend-expect"]
to your Jest config or import that extend-expect file in your test setup file.
Once you have it set up, you're ready to go.
I don't know what's in your styles.active style, but let's assume it's e.g. { borderColor: '#00ffff' }.
Add a testID prop to the View in ImageCircle component, e.g. testID="imageCircleView". Then to test if everything works as you'd expect, you just have to add this to your test:
expect(getByTestId('imageCircleView')).toHaveStyle({ borderColor: '#00ffff' });
And that's it.

Related

state not able to change in child due to rerendering of parent component

I have 3 components with the following flow -
App.js(main parent) --> NotesCard.js(first child) --> NoteEditor.js(second child)
I want to show a user message in NoteEditor.js when the user clicks the button "Save" in NoteEditor.js but this is not working properly. There is a saveMsg state in NoteEditor.js which I want to turn true and display a message.
As soon as I click the save button, NotesArrFunc(currentVidId, videoLink) is getting called which is modifying the state of app.js, hence rerendering it and its child.
I think the issue is that the state in app.js is getting updated and child components are getting rerendered as a result saveMsg is not getting a chance to turn true. How can I turn saveMsg true in NoteEditor on button click so that I can display the message?
App.js - passing videoLink and currentVidId prop to NotesCard
const [videoLink, setvideoLink] = useState(getStoredDataFunc().videoLink)
const [currentVidId, setcurrentVidId] = useState(getStoredDataFunc().currentVidId)
<NotesCard NotesArrFunc={NotesArrFunc} currentVidId={currentVidId} videoLink={videoLink} />
NotesCard.js - passing props from app.js to NoteEditor.js
const NotesCard = ({ videoLink, NotesArrFunc, currentVidId}) => {
return (
<>
<NoteEditor NotesArrFunc={NotesArrFunc} currentVidId={currentVidId} videoLink={videoLink} />
</>
);
};
NoteEditor.js -
const NoteEditor = ({ NotesArrFunc, currentVidId,videoLink}) => {
const [saveMsg, setsaveMsg] = useState(false)
function handleOnClick() {
NotesArrFunc(currentVidId, videoLink)
setsaveMsg(true)
setTimeout(() => {
setsaveMsg(false)
}, 1000);
}
return (
{saveMsg && <h4 style={{ color: "white", textAlign: "center" }}>Your note is saved !</h4>}
<button onClick={handleOnClick}>Save</button>
)

How to properly type navigation passed as props with React-Navigation with expo

How do I properly type navigation passed as props to another component? As per the docs
Each screen component in your app is provided with the navigation prop automatically.
And also,
To type check our screens, we need to annotate the navigation prop and the route prop received by a screen.
type Props = NativeStackScreenProps<RootStackParamList, 'Profile'>;
I have a navigation component with router:
const App: React.FC = () => {
const [userMetrics, setUserMetrics] = useState<UserMetrics>(null);
const Stack = createNativeStackNavigator<RootStackParamList>();
return (
<UserMetricsContext.Provider value={{ userMetrics, setUserMetrics }}>
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Tests" component={Tests} />
</Stack.Navigator>
</NavigationContainer>
</UserMetricsContext.Provider>
);
};
And in Home screen I want to receive navigation prop to pass it down further to form which have a button that will navigate to Tests component and pass form data as params:
interface Props {
navigation: NativeStackScreenProps<RootStackParamList, "Home">;
}
export const Home: React.FC<Props> = ({ navigation }) => {
const { setUserMetrics } =
useContext<IUserMetricsContextType>(UserMetricsContext);
return (
<View style={styles.container}>
<StatusBar style="auto" />
<HealthCheckForm onSubmit={setUserMetrics} navigation={navigation} />
</View>
);
};
and now the problem starts to be visible in form component because typescript is assuming one level too much, I have typed the props like I did in the parent Home component like so:
interface Props {
onSubmit: React.Dispatch<React.SetStateAction<UserMetrics>>;
navigation: NativeStackScreenProps<RootStackParamList, "Home">;
}
and typescript wants me to use it like so:
const submitHandler = (data: UserMetrics) => {
onSubmit(data);
navigation.navigation.navigate("Tests", { userMetrics: data });
console.log(data);
};
This is not working however, the correct - working and navigating usage is
navigation.navigate("Tests", { userMetrics: data });
and when I navigate to the Tests component and pass the params along, I don't know how to receive them in Test component. I am trying to do it analogically like so:
interface Props {
navigation: NativeStackScreenProps<RootStackParamList, "Tests">;
}
export const Tests: React.FC<Props> = ({ navigation }) => {
const { userMetrics } =
useContext<IUserMetricsContextType>(UserMetricsContext);
console.log({ params: navigation.route.params });
return (
<View>
<DisplayList />
</View>
);
};
And I get yet another error about reading properties of undefined.
Thanks
There are solutions for nested Navigators (starting point here: https://reactnavigation.org/docs/nesting-navigators/). However, in this case I would suggest not making your HealthCheckForm have any awareness of the navigation state. Just pass it a standard onSubmit() prop and handle all the navigation within the Home component.
Also as a tip make sure to set up your RootStackParamList correctly so that the "tests" route is expecting {userMetrics: YourDataType}. Here is a random example of setting that up.
export type RootStackParamList = {
myRouteName: undefined;
tests: { userMetrics: MyDataType }; // TS now expects MyDataType on the props for tests route
terminalAndMate: {
departingTerminal: WSFTerminal;
arrivingTerminal: WSFTerminal;
};
...
I would also suggest typing your screen props this way instead of as an interface. NativeStackScreenProps can carry params as defined on the rootStackParamList:
type TestsScreenProps = NativeStackScreenProps<RootStackParamList, "tests">;
With those two changes, the tests screen should have props.route.params, which will contain MyDataType.
try: https://reactnavigation.org/docs/5.x/typescript/
-addition in regards to Seven's comment-
Take a look at the type declaration for NativeStackScreenProps.
export declare type NativeStackScreenProps<ParamList extends ParamListBase, RouteName extends keyof ParamList = string> = {
navigation: NativeStackNavigationProp<ParamList, RouteName>;
route: RouteProp<ParamList, RouteName>;
};
By making the interface as you did, you are saying the type of props for that component is
{ navigation: { navigation: NativeStackNavigation..etc , route: RouteProp }}
You can see that it is double nested and not necessary as the type provided by the library supports all of the functionality you need.
onsubmit
Your onSubmit function would look something like this:
//home.tsx
const onSubmit = (data: YourDataType) => {
props.navigation.navigate("tests", { userMetrics: data});
}
return (
//...stuff
<YourFormComponent onSubmit={onSubmit} />
This way all your navigation is handled by home, which is on the same 'level' as tests, and you keep your navigation a little cleaner.
Shouldn't need any useEffect calls.

React Hover Context Provider

I am trying to build a React Context that will let me know if a area is hovered over or not.
The issue I have currently is with the onMouseOver and onMouseOut which seem to occur a lot so the hover state switches from true to false frequently even though I am hovering over an area.
Here is what I have so far:
const { Provider, Consumer } = createContext({
isHovering: false,
});
Provider.propTypes = {
value: T.shape({
isHovering: T.bool.isRequired,
}),
};
function HoverContextProvider({ children }) {
const [isHovering, setIsHovering] = useState(false);
const value = {
isHovering,
};
return (
<Provider value={value}>
<div
onMouseOver={() => {
setIsHovering(true);
}}
onMouseOut={() => {
setIsHovering(false);
}}
>
{children}
</div>
</Provider>
);
}
HoverContextProvider.propTypes = {
children: T.element.isRequired,
};
export { HoverContextProvider as default, Consumer as HoverContextConsumer };
What I do the is wrap an area with that like this:
<HoverContextProvider> some stuff in here, imagine a page header</HoverContextProvider>
Here is an visual reference:
So as i move my mouse around the green area (which contains lots of text), the isHovering state keeps switching from false to true - I guess because of the text.
Does anyone know how to make this work?

React HOC working on some but not other components

I'm using a HOC component to bind an action to many different types of element, including SVG cells, which, when an onClick is bound normally, it works, but when I use my HOC it returns un-intended results.
Minimally reproducible example: https://codesandbox.io/s/ecstatic-keldysh-3viw0
The HOC component:
export const withReport = Component => ({ children, ...props }) => {
console.log(Component); //this only prints for ListItem elements for some reason
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
console.log('clicked!'); //even this wont work on some.
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}>
{children}
</Component>
);
};
Usage working:
const ListItemWIthReport = withReport(ListItem); //list item from react-mui
{items.map((item, key) => (
<ListItemWithReport report={item.report} key={key} button>
{/* listitem children*/}
</ListItemWithReport>
))}
Usage not working:
const BarWithReport = withReport(Bar); //Bar from recharts
{bars.map((bar, index) => (
<BarWithReport
report={bar.report}
key={index}
dataKey={bar.name}
fill={bar.fill}
/>
))}
The ListItem works 100% as anticipated, however, the bars will not render inside of the BarChart. Similarly, with a PieChart the Cells will actually render, with the correct sizes according to their values, however, props like "fill" do not appear to pass down.
Am I using the HOC incorrectly? I don't see an option other than HOC for the inside of Charts as many types of elements will be considered invalid HTML?
You might be dealing with components that have important static properties that need to be hoisted into the wrapped component or need to have ref forwarding implemented in order for their parent components to handle them. Getting these pieces in place is important, especially when wrapping components where you don't know their internals. That Bar component, for example, does have some static properties. Your HOC is making those disappear.
Here's how you can hoist these static members:
import hoistNonReactStatic from 'hoist-non-react-statics';
export const withReport = Component => {
const EnhancedComponent = props => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}/>
);
};
hoistNonReactStatic(EnhancedComponent, Component);
return EnhancedComponent;
};
Docs on hoisting statics and ref forwarding can be found in this handy guide to HOCs.
There may be some libraries that can take care of all these details for you. One, addhoc, works like this:
import addHOC from 'addhoc';
export const withReport = addHOC(render => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return render({ onClick: handleClick });
});
Of course, if the parent component is checking child components by type explicitly, then you won't be able to use HOCs at all. In fact, it looks like recharts has that issue. Here you can see the chart is defined in terms of child components which are then searched for explicitly by type.
I think your HOC is invalid, because not every wrapper-Component (e.g. HTML element) is basically clickable. Maybe this snipped can clarify what I am trying to say:
const withReport = Component => (props) => {
const handleClick = () => console.log('whatever')
// Careful - your component might not support onClick by default
return <Component onClick={handleClick} {...props} />
// vs.
return <div onClick={handleClick} style={{backgroundColor: 'green'}}>
<Component {...props} />
{props.children}
</div>
}
// Your import from wherever you want
class SomeClass extends React.Component {
render() {
return <span onClick={this.props.onClick}>{this.props.children}</span>
// vs.
return <span style={{backgroundColor: 'red'}}>
{
// Careful - your imported component might not support children by default
this.props.children
}
</span>
}
}
const ReportedListItem = withReport(SomeClass)
ReactDOM.render(<ReportedListItem>
<h2>child</h2>
</ReportedListItem>, mountNode)
You can have the uppers or the unders (separated by vs.) but not crossed. The HOC using the second return (controlled wrapper-Component) is sure more save.
I've used 4 methods successfully to wrap Recharts components.
First Method
Wrap the component in a HOC and use Object.Assign with some overloads. This breaks some animation and difficult to use an active Dot on lines. Recharts grabs some props from components before rendering them. So if the prop isn't passed into the HOC, then it won't render properly.
...
function LineWrapper({
dataOverload,
data,
children,
strokeWidth,
strokeWidthOverload,
isAnimationActive,
dot,
dotOverload,
activeDot,
activeDotOverload,
...rest
}: PropsWithChildren<Props>) {
const defaultDotStroke = 12;
return (
<Line
aria-label="chart-line"
isAnimationActive={false}
strokeWidth={strokeWidthOverload ?? 2}
data={dataOverload?.chartData ?? data}
dot={dotOverload ?? { strokeWidth: defaultDotStroke }}
activeDot={activeDotOverload ?? { strokeWidth: defaultDotStroke + 2 }}
{...rest}
>
{children}
</Line>
);
}
export default renderChartWrapper(Line, LineWrapper, {
activeDot: <Dot r={14} />,
});
const renderChartWrapper = <P extends BP, BP = {}>(
component: React.ComponentType<BP>,
wrapperFC: React.FC<P>,
defaultProps?: Partial<P>
): React.FC<P> => {
Object.assign(wrapperFC, component);
if (defaultProps) {
wrapperFC.defaultProps = wrapperFC.defaultProps ?? {};
Object.assign(wrapperFC.defaultProps, defaultProps);
}
return wrapperFC;
};
Second Method
Use default props to assign values. Any props passed into the HOC will be overridden.
import { XAxisProps } from 'recharts';
import { createStyles } from '#material-ui/core';
import { themeExtensions } from '../../../assets/theme';
const useStyles = createStyles({
tickStyle: {
...themeExtensions.font.graphAxis,
},
});
type Props = XAxisProps;
// There is no actual implementation of XAxis. Recharts render function grabs the props only.
function XAxisWrapper(props: Props) {
return null;
}
XAxisWrapper.displayName = 'XAxis';
XAxisWrapper.defaultProps = {
allowDecimals: true,
hide: false,
orientation: 'bottom',
width: 0,
height: 30,
mirror: false,
xAxisId: 0,
type: 'category',
domain: [0, 'auto'],
padding: { left: 0, right: 0 },
allowDataOverflow: false,
scale: 'auto',
reversed: false,
allowDuplicatedCategory: false,
tick: { style: useStyles.tickStyle },
tickCount: 5,
tickLine: false,
dataKey: 'key',
};
export default XAxisWrapper;
Third Method
I didn't like this so I've worked around it, but you can extend the class.
export default class LineWrapper extends Line {
render(){
return (<Line {...this.props} />
}
}
Fourth Method
I don't have a quick example of this, but I always render the shape or children and provide functions to help. For example, for bar cells I use this:
export default function renderBarCellPattern(cellOptions: CellRenderOptions) {
const { data, fill, match, pattern } = cellOptions;
const id = _uniqueId();
const cells = data.map((d) =>
match(d) ? (
<Cell
key={`cell-${id}`}
strokeWidth={4}
stroke={fill}
fill={`url(#bar-mask-pattern-${id})`}
/>
) : (
<Cell key={`cell-${id}`} strokeWidth={2} fill={fill} />
)
);
return !pattern
? cells
: cells.concat(
<CloneElement<MaskProps>
key={`pattern-${id}`}
element={pattern}
id={`bar-mask-pattern-${id}`}
fill={fill}
/>
);
}
// and
<Bar {...requiredProps}>
{renderBarCellPattern(...cell details)}
</Bar>
CloneElement is just a personal wrapper for Reacts cloneElement().

Why does getComputedStyle() in a JEST test return different results to computed styles in Chrome / Firefox DevTools

I've written a custom button (MyStyledButton) based on material-ui Button.
import React from "react";
import { Button } from "#material-ui/core";
import { makeStyles } from "#material-ui/styles";
const useStyles = makeStyles({
root: {
minWidth: 100
}
});
function MyStyledButton(props) {
const buttonStyle = useStyles(props);
const { children, width, ...others } = props;
return (
<Button classes={{ root: buttonStyle.root }} {...others}>
{children}
</Button>
);
}
export default MyStyledButton;
It is styled using a theme and this specifies the backgroundColor to be a shade of yellow (Specficially #fbb900)
import { createMuiTheme } from "#material-ui/core/styles";
export const myYellow = "#FBB900";
export const theme = createMuiTheme({
overrides: {
MuiButton: {
containedPrimary: {
color: "black",
backgroundColor: myYellow
}
}
}
});
The component is instantiated in my main index.js and wrapped in the theme.
<MuiThemeProvider theme={theme}>
<MyStyledButton variant="contained" color="primary">
Primary Click Me
</MyStyledButton>
</MuiThemeProvider>
If I examine the button in Chrome DevTools the background-color is "computed" as expected. This is also the case in Firefox DevTools.
However when I write a JEST test to check the background-color and I query the DOM node style òf the button using getComputedStyles() I get transparent back and the test fails.
const wrapper = mount(
<MyStyledButton variant="contained" color="primary">
Primary
</MyStyledButton>
);
const foundButton = wrapper.find("button");
expect(foundButton).toHaveLength(1);
//I want to check the background colour of the button here
//I've tried getComputedStyle() but it returns 'transparent' instead of #FBB900
expect(
window
.getComputedStyle(foundButton.getDOMNode())
.getPropertyValue("background-color")
).toEqual(myYellow);
I've included a CodeSandbox with the exact problem, the minimum code to reproduce and the failing JEST test.
I've gotten closer, but not quite at a solution yet.
The main issue is that MUIButton injects a tag to the element to power the styles. This isn't happening in your unit test. I was able to get this to work by using the createMount that the material tests use.
After this fix, the style is correctly showing up. However, the computed style still doesn't work. It looks like others have run into issues with enzyme handling this correctly - so I'm not sure if it's possible.
To get to where I was, take your test snippet, copy this to the top, and then change your test code to:
const myMount = createMount({ strict: true });
const wrapper = myMount(
<MuiThemeProvider theme={theme}>
<MyStyledButton variant="contained" color="primary">
Primary
</MyStyledButton>
</MuiThemeProvider>
);
class Mode extends React.Component {
static propTypes = {
/**
* this is essentially children. However we can't use children because then
* using `wrapper.setProps({ children })` would work differently if this component
* would be the root.
*/
__element: PropTypes.element.isRequired,
__strict: PropTypes.bool.isRequired,
};
render() {
// Excess props will come from e.g. enzyme setProps
const { __element, __strict, ...other } = this.props;
const Component = __strict ? React.StrictMode : React.Fragment;
return <Component>{React.cloneElement(__element, other)}</Component>;
}
}
// Generate an enhanced mount function.
function createMount(options = {}) {
const attachTo = document.createElement('div');
attachTo.className = 'app';
attachTo.setAttribute('id', 'app');
document.body.insertBefore(attachTo, document.body.firstChild);
const mountWithContext = function mountWithContext(node, localOptions = {}) {
const strict = true;
const disableUnnmount = false;
const localEnzymeOptions = {};
const globalEnzymeOptions = {};
if (!disableUnnmount) {
ReactDOM.unmountComponentAtNode(attachTo);
}
// some tests require that no other components are in the tree
// e.g. when doing .instance(), .state() etc.
return mount(strict == null ? node : <Mode __element={node} __strict={Boolean(strict)} />, {
attachTo,
...globalEnzymeOptions,
...localEnzymeOptions,
});
};
mountWithContext.attachTo = attachTo;
mountWithContext.cleanUp = () => {
ReactDOM.unmountComponentAtNode(attachTo);
attachTo.parentElement.removeChild(attachTo);
};
return mountWithContext;
}

Categories

Resources