Jest assert on style after animate is performed - reactjs

I have a React Application using Jest and React Testing Library for unit testing. I have difficulties asserting that a component's style has changed. My component takes in an isSelected boolean and performs an animation based on that.
My component: (Using framer-motion)
const MotionImage = motion(Image);
const SCALE_SIZE = 1.5;
export interface IPreviewer{
isSelected: boolean;
}
export function Previewer({
isSelected,
}: IPreviewer): JSX.Element {
return (
<MotionImage
animate={{
scale: isSelected ? SCALE_SIZE : 1,
}}
/>
);
}
My test:
test("Component when isSelected highlights the image", () => {
const { debug } = render(
<Previewer
isSelected
/>
);
const image = screen.getByRole("img");
debug(image);
expect(image).toHaveStyle("transform: scale(1.5) translateZ(0)");
});
The test debug giving me:
<img
class="chakra-image css-yk396h"
src="http://placeimg.com/222.07846627773333/3.4102996091771587"
style="transform: none;"
/>
As you can see the img is found but the style is not as expected: style="transform: none;"
I will appreciate any suggestions on how to test this right?
Note
Setting initial={false} to the component itself will make the test pass but will lose the initial animation so I will avoid that.
Introducing a timeOut promise inside the test before asserting will work, but I would like to avoid that as well.

waitFor should help you to wait for the styles. https://testing-library.com/docs/dom-testing-library/api-async/#waitfor

Related

How do I turn off Framer Motion animations in mobile view?

I am trying to create a React website using Framer Motion, the problem is that my animation looks good in desktop view, but not really in mobile. Now I want to disable the animation. How do I do this?
Without knowing further details, I would recommend using Variants for this.
Inside your functional component, create a variable that checks for mobile devices. Then, only fill the variants if this variable is false.
Note that it doesn't work when you resize the page. The component must get rerendered.
I've created a codesandbox for you to try it out!
For further information on Variants, check this course
Another simple way I just did myself when this exact question came up. Below we are using a ternary operatory to generate a object which we then spread using the spread syntax
const attributes = isMobile ? {
drag: "x",
dragConstraints: { left: 0, right: 0 },
animate: { x: myVariable },
onDragEnd: myFunction
} : { onMouseOver, onMouseLeave };
<motion.div {...attributes}> {/* stuff */} </motion.div>
As you can see I want onMouseEnter & onMouseLeave on desktop with no animations. On mobile I want the opposite. This is working of me perfectly.
Hope this helps too.
Daniel
This is how we've done it:
import {
type ForwardRefComponent,
type HTMLMotionProps,
motion as Motion,
} from 'framer-motion';
import { forwardRef } from 'react';
const ReducedMotionDiv: ForwardRefComponent<
HTMLDivElement,
HTMLMotionProps<'div'>
> = forwardRef((props, ref) => {
const newProps = {
...props,
animate: undefined,
initial: undefined,
transition: undefined,
variants: undefined,
whileDrag: undefined,
whileFocus: undefined,
whileHover: undefined,
whileInView: undefined,
whileTap: undefined,
};
return <Motion.div {...newProps} ref={ref} />;
});
export const motion = new Proxy(Motion, {
get: (target, key) => {
if (key === 'div') {
return ReducedMotionDiv;
}
// #ts-expect-error - This is a proxy, so we can't be sure what the key is.
return target[key];
},
});
export {
AnimatePresence,
type Variants,
type HTMLMotionProps,
type MotionProps,
type TargetAndTransition,
type Transition,
type Spring,
} from 'framer-motion';
Same principle as other answers, just complete example.

How can I add a fade-in animation for Nextjs/Image when it loads?

I'm using next/image, which works great, except the actual image loading in is super jarring and there's no animation or fade in. Is there a way to accomplish this? I've tried a ton of things and none of them work.
Here's my code:
<Image
src={source}
alt=""
layout="responsive"
width={750}
height={height}
className="bg-gray-400"
loading="eager"
/>
According to the docs I can use the className prop, but those are loaded immediately and I can't figure out any way to apply a class after it's loaded.
I also tried onLoad, and according to this ticket, it isn't supported:
https://github.com/vercel/next.js/issues/20368
NextJS now supports placeholder. You can fill the blurDataURL property with the base64 string of the image which you can easily get using the lib plaiceholder on getServerSideProps or getStaticProps. Then to make the transition smoothly you can add transition: 0.3s;
Quick sample:
export const UserInfo: React.FC<TUserInfo> = ({ profile }) => {
return (
<div className="w-24 h-24 rounded-full overflow-hidden">
<Image
src={profile.image}
placeholder="blur"
blurDataURL={profile.blurDataURL}
width="100%"
height="100%"
/>
</div>
);
};
export async function getServerSideProps(props: any) {
const { username } = props.query;
const userProfileByName = `${BASE_URL}/account/user_profile_by_user_name?user_name=${username}`;
const profileResponse = await (await fetch(userProfileByName)).json();
const profile = profileResponse?.result?.data[0];
const { base64 } = await getPlaiceholder(profile.profile_image);
return {
props: {
profile: {
...profile,
blurDataURL: base64,
},
},
};
}
index.css
img {
transition: 0.3s;
}
======== EDIT ==========
If you have the image in the public folder for ex, you don't need to do the above steps, just statically import the asset and add the placeholder type. NextJS will do the rest. Also, make sure to make good use of the size property to load the correct image size for the viewport and use the priority prop for above-the-fold assets. Example:
import NextImage from 'next/image'
import imgSrc from '/public/imgs/awesome-img.png'
return (
...
<NextImage
src={imgSrc}
placeholder='blur'
priority
layout="fill"
sizes="(min-width: 1200px) 33vw, (min-width: 768px) 50vw, 100vw"
/>
)
I wanted to achieve the same thing and tried to use the onLoad event, therefore. The Image component of nextJs accepts this as prop, so this was my result:
const animationVariants = {
visible: { opacity: 1 },
hidden: { opacity: 0 },
}
const FadeInImage = props => {
const [loaded, setLoaded] = useState(false);
const animationControls = useAnimation();
useEffect(
() => {
if(loaded){
animationControls.start("visible");
}
},
[loaded]
);
return(
<motion.div
initial={"hidden"}
animate={animationControls}
variants={animationVariants}
transition={{ ease: "easeOut", duration: 1 }}
>
<Image
{...p}
onLoad={() => setLoaded(true)}
/>
</motion.div>
);
}
However, the Image does not always fade-in, the onLoad event seems to be triggered too early if the image is not cached already. I suspect this is a bug that will be fixed in future nextJS releases. If someone else finds a solution, please keep me updated!
The solution above however works often, and since onLoad gets triggered every time, it does not break anything.
Edit: This solution uses framer-motion for the animation. This could also be replaced by any other animation library or native CSS transitions
You could try use next-placeholder to achieve this sort of effect
Yes, its possible to capture the event where the actual image loads. I found an answer to this on Reddit and wanted to repost it here for others like me searching for an anwser.
"To get onLoad to work in the NextJS image component you need make sure it's not the 1x1 px they use as placeholder that is the target.
const [imageIsLoaded, setImageIsLoaded] = useState(false)
<Image
width={100}
height={100}
src={'some/src.jpg'}
onLoad={event => {
const target = event.target;
// next/image use an 1x1 px git as placeholder. We only want the onLoad event on the actual image
if (target.src.indexOf('data:image/gif;base64') < 0) {
setImageIsLoaded(true)
}
}}
/>
From there you can just use the imageIsLoaded boolean to do some fadein with something like the Framer Motion library.
Source: https://www.reddit.com/r/nextjs/comments/lwx0j0/fade_in_when_loading_nextimage/

Testing component with lodash.debounce delay failing

I have a rich text editor input field that I wanted to wrap with a debounced component. Debounced input component looks like this:
import { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';
const useDebounce = (callback, delay) => {
const debouncedFn = useCallback(
debounce((...args) => callback(...args), delay),
[delay] // will recreate if delay changes
);
return debouncedFn;
};
function DebouncedInput(props) {
const [value, setValue] = useState(props.value);
const debouncedSave = useDebounce((nextValue) => props.onChange(nextValue), props.delay);
const handleChange = (nextValue) => {
setValue(nextValue);
debouncedSave(nextValue);
};
return props.renderProps({ onChange: handleChange, value });
}
export default DebouncedInput;
I am using DebouncedInput as a wrapper component for MediumEditor:
<DebouncedInput
value={task.text}
onChange={(text) => onTextChange(text)}
delay={500}
renderProps={(props) => (
<MediumEditor
{...props}
id="task"
style={{ height: '100%' }}
placeholder="Task text…"
disabled={readOnly}
key={task.id}
/>
)}
/>;
MediumEditor component does some sanitation work that I would like to test, for example stripping html tags:
class MediumEditor extends React.Component {
static props = {
id: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
disabled: PropTypes.bool,
uniqueID: PropTypes.any,
placeholder: PropTypes.string,
style: PropTypes.object,
};
onChange(text) {
this.props.onChange(stripHtml(text) === '' ? '' : fixExcelPaste(text));
}
render() {
const {
id,
value,
onChange,
disabled,
placeholder,
style,
uniqueID,
...restProps
} = this.props;
return (
<div style={{ position: 'relative', height: '100%' }} {...restProps}>
{disabled && (
<div
style={{
position: 'absolute',
width: '100%',
height: '100%',
cursor: 'not-allowed',
zIndex: 1,
}}
/>
)}
<Editor
id={id}
data-testid="medium-editor"
options={{
toolbar: {
buttons: ['bold', 'italic', 'underline', 'subscript', 'superscript'],
},
spellcheck: false,
disableEditing: disabled,
placeholder: { text: placeholder || 'Skriv inn tekst...' },
}}
onChange={(text) => this.onChange(text)}
text={value}
style={{
...style,
background: disabled ? 'transparent' : 'white',
borderColor: disabled ? 'grey' : '#FF9600',
overflowY: 'auto',
color: '#444F55',
}}
/>
</div>
);
}
}
export default MediumEditor;
And this is how I am testing this:
it('not stripping html tags if there is text', async () => {
expect(editor.instance.state.text).toEqual('Lorem ipsum ...?');
const mediumEditor = editor.findByProps({ 'data-testid': 'medium-editor' });
const newText = '<p><b>New text, Flesk</b></p>';
mediumEditor.props.onChange(newText);
// jest.runAllTimers();
expect(editor.instance.state.text).toEqual(newText);
});
When I run this test I get:
Error: expect(received).toEqual(expected) // deep equality
Expected: "<p><b>New text, Flesk</b></p>"
Received: "Lorem ipsum ...?"
I have also tried running the test with jest.runAllTimers(); before checking the result, but then I get:
Error: Ran 100000 timers, and there are still more! Assuming we've hit an infinite recursion and bailing out...
I have also tried with:
jest.advanceTimersByTime(500);
But the test keeps failing, I get the old state of the text.
It seems like the state just doesn't change for some reason, which is weird since the component used to work and the test were green before I had them wrapped with DebounceInput component.
The parent component where I have MediumEditor has a method onTextChange that should be called from the DebounceInput component since that is the function that is being passed as the onChange prop to the DebounceInput, but in the test, I can see this method is never reached. In the browser, everything works fine, so I don't know why it is not working in the test?
onTextChange(text) {
console.log('text', text);
this.setState((state) => {
return {
task: { ...state.task, text },
isDirty: true,
};
});
}
On inspecting further I could see that the correct value is being passed in the test all the way to handleChange in DebouncedInput. So, I suspect, there are some problems with lodash.debounce in this test. I am not sure if I should mock this function or does mock come with jest?
const handleChange = (nextValue) => {
console.log(nextValue);
setValue(nextValue);
debouncedSave(nextValue);
};
This is where I suspect the problem is in the test:
const useDebounce = (callback, delay) => {
const debouncedFn = useCallback(
debounce((...args) => callback(...args), delay),
[delay] // will recreate if delay changes
);
return debouncedFn;
};
I have tried with mocking debounce like this:
import debounce from 'lodash.debounce'
jest.mock('lodash.debounce');
debounce.mockImplementation(() => jest.fn(fn => fn));
That gave me error:
TypeError: _lodash.default.mockImplementation is not a function
How should I fix this?
I'm guessing that you are using enzyme (from the props access).
In order to test some code that depends on timers in jest:
mark to jest to use fake timers with call to jest.useFakeTimers()
render your component
make your change (which will start the timers, in your case is the state change), pay attention that when you change the state from enzyme, you need to call componentWrapper.update()
advance the timers using jest.runOnlyPendingTimers()
This should work.
Few side notes regarding testing react components:
If you want to test the function of onChange, test the immediate component (in your case MediumEditor), there is no point of testing the entire wrapped component for testing the onChange functionality
Don't update the state from tests, it makes your tests highly couple to specific implementation, prove, rename the state variable name, the functionality of your component won't change, but your tests will fail, since they will try to update a state of none existing state variable.
Don't call onChange props (or any other props) from test. It makes your tests more implementation aware (=high couple with component implementation), and actually they doesn't check that your component works properly, think for example that for some reason you didn't pass the onChange prop to the input, your tests will pass (since your test is calling the onChange prop), but in real it won't work.
The best approach of component testing is to simulate actions on the component like your user will do, for example, in input component, simulate a change / input event on the component (this is what your user does in real app when he types).

Moving slider with Cypress

I've got a Slider component from rc-slider and I need Cypress to set the value of it.
<Slider
min={5000}
max={40000}
step={500}
value={this.state.input.amount}
defaultValue={this.state.input.amount}
className="sliderBorrow"
onChange={(value) => this.updateInput("amount",value)}
data-cy={"input-slider"}
/>
This is my Cypress code:
it.only("Changing slider", () => {
cy.visit("/");
cy.get(".sliderBorrow")
.invoke("val", 23000)
.trigger("change")
.click({ force: true })
});
What I've tried so far does not work.
Starting point of slider is 20000, and after test runs it goes to 22000, no matter what value I pass, any number range.
Looks like it used to work before, How do interact correctly with a range input (slider) in Cypress? but not anymore.
The answer is very and very simple. I found the solution coincidentally pressing enter key for my another test(date picker) and realized that pressing left or right arrow keys works for slider.
You can achieve the same result using props as well. The only thing you need to do is to add this dependency: cypress-react-selector and following instructions here: cypress-react-selector
Example of using {rightarrow}
it("using arrow keys", () => {
cy.visit("localhost:3000");
const currentValue = 20000;
const targetValue = 35000;
const increment = 500;
const steps = (targetValue - currentValue) / increment;
const arrows = '{rightarrow}'.repeat(steps);
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 20000)
.type(arrows)
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 35000)
})
#darkseid's answer helped guide me reach an optimal solution.
There are two steps
Click the slider's circle, to move the current focus on the slider.
Press the keyboard arrow buttons to reach your desired value.
My slider jumps between values on the sliders, therefore this method would work. (I am using Ion range slider)
This method doesn't require any additional depedency.
// Move the focus to slider, by clicking on the slider's circle element
cy.get(".irs-handle.single").click({ multiple: true, force: true });
// Press right arrow two times
cy.get(".irs-handle.single").type(
"{rightarrow}{rightarrow}"
);
You might be able to tackle this using Application actions, provided you are able to modify the app source code slightly.
Application actions give the test a hook into the app that can be used to modify the internal state of the app.
I tested it with a Function component exposing setValue from the useState() hook.
You have used a Class component, so I guess you would expose this.updateInput() instead, something like
if (window.Cypress) {
window.app = { updateInput: this.updateInput };
}
App: index.js
import React, { useState } from 'react';
import { render } from 'react-dom';
import './style.css';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
function App() {
const [value, setValue] = useState(20000);
// Expose the setValue() method so that Cypress can set the app state
if (window.Cypress) {
window.app = { setValue };
}
return (
<div className="App">
<Slider
min={5000}
max={40000}
step={500}
value={value}
defaultValue={value}
className="sliderBorrow"
onChange={val => setValue(val)}
data-cy={"input-slider"}
/>
<div style={{ marginTop: 40 }}><b>Selected Value: </b>{value}</div>
</div>
);
}
render(<App />, document.getElementById('root'));
Test: slider.spec.js
The easiest way I found assert the value in the test is to use the aria-valuenow attribute of the slider handle, but you may have another way of testing that the value has visibly changed on the page.
describe('Slider', () => {
it("Changing slider", () => {
cy.visit("localhost:3000");
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 20000)
cy.window().then(win => {
win.app.setValue(35000);
})
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 35000)
})
})
For whoever comes across this with Material UI/MUI 5+ Sliders:
First off, this github issue and comment might be useful: https://github.com/cypress-io/cypress/issues/1570#issuecomment-606445818.
I tried changing the value by accessing the input with type range that is used underneath in the slider, but for me that did not do the trick.
My solution with MUI 5+ Slider:
<Slider
disabled={false}
step={5}
marks
data-cy="control-percentage"
name="control-percentage"
defaultValue={0}
onChange={(event, newValue) =>
//Handle change
}
/>
What is important here is the enabled marks property. This allowed me to just click straight on the marks in the cypress test, which of course can also be abstracted to a support function.
cy.get('[data-cy=control-percentage]').within(() => {
// index 11 represents 55 in this case, depending on your step setting.
cy.get('span[data-index=11]').click();
});
I got this to work with the popular react-easy-swipe:
cy.get('[data-cy=week-picker-swipe-container]')
.trigger('touchstart', {
touches: [{ pageY: 0, pageX: 0 }]
})
.trigger('touchmove', {
touches: [{ pageY: 0, pageX: -30 }]
})

React testing library: Test styles (specifically background image)

I'm building a React app with TypeScript. I do my component tests with react-testing-library.
I'm buildilng a parallax component for my landing page.
The component is passed the image via props and sets it via JSS as a background image:
<div
className={parallaxClasses}
style={{
backgroundImage: "url(" + image + ")",
...this.state
}}
>
{children}
</div>
Here is the unit test that I wrote:
import React from "react";
import { cleanup, render } from "react-testing-library";
import Parallax, { OwnProps } from "./Parallax";
afterEach(cleanup);
const createTestProps = (props?: object): OwnProps => ({
children: null,
filter: "primary",
image: require("../../assets/images/bridge.jpg"),
...props
});
describe("Parallax", () => {
const props = createTestProps();
const { getByText } = render(<Parallax {...props} />);
describe("rendering", () => {
test("it renders the image", () => {
expect(getByText(props.image)).toBeDefined();
});
});
});
But it fails saying:
● Parallax › rendering › it renders the image
Unable to find an element with the text: bridge.jpg. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
<body>
<div>
<div
class="Parallax-parallax-3 Parallax-primaryColor-4"
style="background-image: url(bridge.jpg); transform: translate3d(0,0px,0);"
/>
</div>
</body>
16 | describe("rendering", () => {
17 | test("it renders the image", () => {
> 18 | expect(getByText(props.image)).toBeDefined();
| ^
19 | });
20 | });
21 | });
at getElementError (node_modules/dom-testing-library/dist/query-helpers.js:30:10)
at getAllByText (node_modules/dom-testing-library/dist/queries.js:336:45)
at firstResultOrNull (node_modules/dom-testing-library/dist/query-helpers.js:38:30)
at getByText (node_modules/dom-testing-library/dist/queries.js:346:42)
at Object.getByText (src/components/Parallax/Parallax.test.tsx:18:14)
How can I test that the image is being set as a background image correctly with Jest and react-testing-library?
getByText won't find the image or its CSS. What it does is to look for a DOM node with the text you specified.
In your case, I would add a data-testid parameter to your background (<div data-testid="background">) and find the component using getByTestId.
After that you can test like this:
expect(getByTestId('background')).toHaveStyle(`background-image: url(${props.image})`)
Make sure you install #testing-library/jest-dom in order to have toHaveStyle.
If you want to avoid adding data-testid to your component, you can use container from react-testing-library.
const {container} = render(<Parallax {...props})/>
expect(container.firstChild).toHaveStyle(`background-image: url(${props.image})`)
This solution makes sense for your component test, since you are testing the background-image of the root node. However, keep in mind this note from the docs:
If you find yourself using container to query for rendered elements then you should reconsider! The other queries are designed to be more resiliant to changes that will be made to the component you're testing. Avoid using container to query for elements!
in addition to toHaveStyle JsDOM Matcher, you can use also style property which is available to the current dom element
Element DOM API
expect(getByTestId('background').style.backgroundImage).toEqual(`url(${props.image})`)
also, you can use another jestDOM matcher
toHaveAttribute Matcher
expect(getByTestId('background')).toHaveAttribute('style',`background-image: url(${props.image})`)
Simple solution for testing Component css with react-testing-library. this is helpful for me it is working perfect.
test('Should attach background color if user
provide color from props', () => {
render(<ProfilePic name="Any Name" color="red" data-
testid="profile"/>);
//or can fetch the specific element from the component
const profile = screen.getByTestId('profile');
const profilePicElem = document.getElementsByClassName(
profile.className,
);
const style = window.getComputedStyle(profilePicElem[0]);
//Assertion
expect(style.backgroundColor).toBe('red');
});

Resources