Testing `img.onLoad`/`img.onError` using Jest and React Testing Library - javascript

Out team ran into a scenario this afternoon when writing a React Testing Library (RTL) test with Jest for our <Avatar /> component. Specifically, we wanted to test that the <img /> tag was removed from the DOM when it fails to load (onError is triggered) to match the expected final view of component. For some reason, using fireEvent on the <img /> DOM Element was not immediately obvious to us and we didn't find this explicit solution online so we wanted share. As you can imagine, this will work for other events such as onLoad as well. More about RTL Events.
it('should render with only initials when avatar image is NOT found', async() => {
const { container } = render(<Avatar {...defaultMocks} />);
const avatarImg = container.querySelector('img');
expect(avatarImg).toBeInTheDocument();
fireEvent(avatarImg, new Event('error'));
expect(avatarImg).not.toBeInTheDocument();
});

get the image using testId or role then use fireEvent to call onLoad or OnError functions respectively
const image = getByRole('img')
fireEvent.load(image);
for onError
fireEvent.error(image)

Explanation
<Avatar /> is rendered with default props, and in our case, contains an <img /> tag pointing to an optional profile image endpoint for the user.
We then use fireEvent on that DOM element, triggering the onError function that was bound at render time mocking a failed/404 case where a user avatar was not set.
Finally we can now expect the <img /> was removed based on the logic within <Avatar />

Related

Updated: How can I insert a script at a specific position in the page of a NextJS app?

I am trying to get a script embedding onto my next.js application, however it is at the end of the page, instead of where I place it.
I already took the solution mentioned in How to precisely control where script tag is inserted using Next Script, but the issue still persists. I also tried to embed it with the application script method described here https://nextjs.org/docs/basic-features/script, but I get the same result.
My helloworld.tsx in pages looks like this:
import React, { useRef } from 'react'
import Script from 'next/script'
export const HelloWorld = () => {
const containerRef = useRef(null)
function moveScript() {
containerRef.current.appendChild(this)
}
return (
<>
<p>This goes before the embedding</p>
<div ref={containerRef} id="script-container">
<Script strategy="lazyOnload" id="asciicast-aMFGH8jU7O7uo94YZyWJEZtnO" type="text/javascript" src="https://asciinema.org/a/aMFGH8jU7O7uo94YZyWJEZtnO.js" async onLoad={moveScript}/>
</div>
<p>This goes after the embedding <br /></p>
</>
);
};
export default HelloWorld;
On the resulting page the embedding is at the bottom of the page, however I expect it to be between the two paragraph.
What am I missing?
Why don't you use an useEffect to load the script, and then append it?
Since useEffect runs after the render of the component, you could either:
[1] use it to load the script in it (assuring that the script load goes after the component rendering), or
[2] use it afterwards to move (append) the script to the specific ref. I can't test your code right now, but I'm pretty sure that might help you solve the problem you're having.

How to capture paste event anywhere on page in React?

Having looked at other stack overflow questions and on Google, I haven't been able to find an answer to this.
Background:
I'm currently re-writing a jquery application in React. Previously I was just adding a paste event listener to the body of the page. That seemed to allow me to capture any paste event the user would do.
Questions:
In React, with the code below in my App.js file, on initial load of the page it doesn't allow me to capture paste events. Only after clicking somewhere on the page does it then work when I paste. I realise I could just have an input and an onPaste attribute, but I need to be able to capture automatically.
In terms of passing the pasted text down the component tree, with the ability to edit the text from a lower component, am I right in thinking it's best to pass a callback as a prop down from the top-level component so the state is always updated using a function in the top-level component?
Currently this is what I have:
// App.js
import TopBar from "./components/TopBar/TopBar";
import Wrapper from "./components/Wrapper/Wrapper";
import AppContainer from "./components/AppContainer/AppContainer";
function App() {
const [inputText, setInputText] = useState(false);
return (
<div
onPaste={(e) => setInputText(e.clipboardData.getData("Text"))}
className="App"
>
<TopBar />
<Wrapper>
<h2>New Flashcard</h2>
<AppContainer inputText={inputText} setInputText={setInputText} />
</Wrapper>
</div>
);
}
export default App;
Your listener will only work when the specific div is focused, therefore it works on the second attempt. Instead, add the listener to body element or window object:
function Component() {
const [inputText, setInputText] = useState(false);
useEffect(() => {
window.addEventListener("paste", (e) => {
setInputText(e.clipboardData.getData("text"));
});
return () => {
window.removeEventListener("paste");
};
}, []);
return <div className="App">{inputText}</div>;
}
am I right in thinking it's best to pass a callback
No, it depends on the depth, please read about Context API and research on an anti-pattern called "props-drilling".

User UI feedback after clicking on a react-router-dom <Link/>?

Here's what I'm dealing with:
Single page app built with react + react-router-dom
User clicks on a <Link to={"/new-page-route"}/>
URL changes and my <Route/> starts rendering a new <Component/>
I mean, React is fast, but my new component takes a while to render because it's a whole new page. So it should take something between 200 and 400ms. And if I get no UI feedback, it feels that my click has not worked.
I need some kind of UI feedback so my user's know their click has been fired and something is going on. I don't think I need loaders or anything, but something to show that the click has been "accepted" by the UI.
In theory that could be handled by CSS using:
-webkit-tap-highlight-color: #SOMECOLOR
active: #SOMECOLOR
But somehow, when the URL changes and the new render begins, the browser is not being able to paint those CSS results to the screen, at least this is the behavior on Chrome and Firefox. It gets kind of weird, sometimes I see the tap-highlight and the active-change but almost always I don't see it.
NOTE: This is not the 300ms default delay on mobile that waits for the double tap. I've dealt with that using the appropriate tags.
What I thought about doing is:
Stop using the <Link/> component and use a normal <a/>.
Create a clicked state to be update after the click event
Call event.preventDefault() to prevent normal behavior of the <a/> navigation
Use the clicked state to render some new styles for the UI feedback
Fire history.push("/new-page-route") on a useEffect after clicked state has become true
Something like:
const newUrl = "/new-page-route";
const [clicked,setClicked] = useState(false);
const history = useHistory(); // HOOK FROM react-router-dom
function handleLinkClick(event) {
event.preventDefault();
setClicked(true);
}
useEffect(() => {
if (clicked === true) {
history.push(newUrl);
// OR MAYBE ADD A TIMEOUT TO BE EXTRA SURE THAT THE FEEDBACK WILL BE PAINTED
// BECAUSE I'VE SEEN THE BROWSER NOT BEING ABLE TO PAINT IF I DON'T GIVE IT SOME EXTRA TIME
setTimeout(() => history.push(newUrl),100);
}
},[clicked,history]);
// USE THE clicked STATE TO RENDER THE UI FEEDBACK (CHANGE TEXT COLOR, WHATEVER I NEED);
QUESTION
Has anyone had this issue before? What is a good way of solving this? I guess that in theory the browser should be able to paint before the new render begins, but this is not what I'm getting.
SANDBOX WITH THE ISSUE
https://codesandbox.io/s/nice-monad-4fwoc <=== CLICK HERE TO SEE THE CODE
https://4fwoc.csb.app <=== CLICK HERE TO SEE THE RESULT ONLY
On desktop: I'm able to see the BLUE background for the active state when clicking the link
On mobile: I don't see any CSS change when clicking the links. Unless I tap AND HOLD
Comments: Imagine that your component takes a while to render. Without any UI feedback, it feels that you haven't clicked at all, even though it is rendering on background.
import React from "react";
import { Link, Switch, Route } from "react-router-dom";
import styled from "styled-components";
import "./styles.css";
const HOME = "/";
const ROUTE1 = "/route1";
const ROUTE2 = "/route2";
const LS = {};
// REGULAR CSS RULES FOR THE className="link" ARE ON "./styles.css"
LS.Link_LINK = styled(Link)`
color: black;
-webkit-tap-highlight-color: red;
&:active {
background-color: blue;
}
`;
export default function App() {
return (
<React.Fragment>
<div>
<Switch>
<Route exact path={HOME} component={Home} />
<Route exact path={ROUTE1} component={Component1} />
<Route exact path={ROUTE2} component={Component2} />
</Switch>
</div>
</React.Fragment>
);
}
function Home() {
return (
<React.Fragment>
<div>I am Home</div>
<LS.Link_LINK to={ROUTE1}>Route 1 (using styled-components)</LS.Link_LINK>
<br />
<Link className={"link"} to={ROUTE1}>
Route 1 (using regular CSS)
</Link>
</React.Fragment>
);
}
function Component1() {
return (
<React.Fragment>
<div>I am Component1</div>
<LS.Link_LINK to={ROUTE2}>Route 2 (using styled-components)</LS.Link_LINK>
<br />
<Link className={"link"} to={ROUTE2}>
Route 2 (using regular CSS)
</Link>
</React.Fragment>
);
}
function Component2() {
return (
<React.Fragment>
<div>I am Component2</div>
<LS.Link_LINK to={HOME}>Home (using styled-components)</LS.Link_LINK>
<br />
<Link className={"link"} to={HOME}>
Home (using regular CSS)
</Link>
</React.Fragment>
);
}
You should use two loading phases.
Use lazy
loading
when loading the "heavy" page and display a
skeleton
component as a fallback to show the user that something is loading.
Use a component/redux state loading indicator for when your page is
loading data from apis
Trying to achive that with react-router links is not the best way because routing happends instantly, it is not the thing that stalls your application. Api calls and multiple re-renders cause an application to behave slowly.

Responding to a click on a Vue element programmatically

Essentially, I want my Vue instance to respond to a click on an uploaded thumbnail.
I'm using the FineUploader Vue package with the template layout per the docs (see end of the question). Upon uploading an image, a tree like this is outputted:
<Root>
<Gallery>
<Thumbnail>
</Gallery>
</Root>
Coming from a jQuery background I really have no idea about the 'correct' way to go about this given that the Thumbnail Template is defined by the package already, and so I'm not creating my own Thumbnail template. I know that I can access elements like this:
let thumb = this.$el.querySelector('.vue-fine-uploader-thumbnail');
And perhaps a listener
thumb.addEventListener('click', function() {
alert('I got clicked');
});
But dealing with the Vue instance being re-rendered etc. I'm not familiar with.
Vue Template:
<template>
<Gallery :uploader="uploader" />
</template>
<script>
import FineUploaderTraditional from 'fine-uploader-wrappers'
import Gallery from 'vue-fineuploader/gallery'
export default {
components: {
Gallery
},
data () {
const uploader = new FineUploaderTraditional({
options: {/*snip*/}
})
return {
uploader
}
}
}
</script>
In order to respond to click events you add the v-on:click (or it's short form: #click) to whatever tag you want.
If you have elements that are nested that respond to the click event you might experience that a click on a child triggers a parents click event. To prevent this you add #click.stop instead, so that it doesn't trigger the parents click.
So you would have something along the lines of:
<Gallery #click="myFunction" />

"Warning: react-modal: App element is not defined. Please use `Modal.setAppElement(el)` or set `appElement={el}`"

How do I fix this warning in console of a React app using the react-modal package:
Warning: react-modal: App element is not defined. Please use Modal.setAppElement(el) or set appElement={el}
I have not been successful at figuring out what el is supposed to be.
Context:
in my App.js root component file:
...
import Modal from 'react-modal';
...
class App extends Component {
...
render(){
...
<Modal
className="modal"
overlayClassName="overlay"
isOpen={foodModalOpen}
onRequestClose={this.closeFoodModal}
contentLabel="Modal"
>
...
}
}
Where ... indicates code not shown.
Everything works fine, but when the Modal is opened, the following Warning appears in my console:
index.js:2177 Warning: react-modal: App element is not defined. Please use Modal.setAppElement(el) or set appElement={el}. This is needed so screen readers don't see main content when modal is opened. It is not recommended, but you can opt-out by setting ariaHideApp={false}.
In the react-modal docs all I can find is the following:
App Element
The app element allows you to specify the portion of your app that should be hidden (via aria-hidden) to prevent assistive technologies such as screenreaders from reading content outside of the content of your modal.
If you are doing server-side rendering, you should use this property.
It can be specified in the following ways:
DOMElement
Modal.setAppElement(appElement);
query selector - uses the first element found if you pass in a class.
Modal.setAppElement('#your-app-element');
Unfortunately, this has not helped! I cannot figure out what el is supposed to represent.
Here are some of the many property variations I have tried adding to my Modal component:
`appElement={el}`,
`appElement="root"` where `root` is the id that my App component is injected into
`appElement={'root'}`
`appElement="div"`,
`appElement={<div>}`,
`appElement={"div"}`
I've also tried calling Modal.setAppElement('root'); from inside src/index.js, where root is the root element that my App component is injected into, and index.js is where I do that.
Add ariaHideApp={false} to Modal attributes.
This should work:
<Modal isOpen={!!props.selectedOption}
onRequestClose={props.clearSelectedOption}
ariaHideApp={false}
contentLabel="Selected Option"
>
</Modal>
Some solutions are given in react-modal issue #133:
The problem lies here:
Depending on when it evaluates react-modal#1.6.5:/lib/helpers/ariaAppHider.js#L1:
document.body does not exist yet and it will resolve to undefined || null.
if Modal.setAppElement() is called with null or not called at all with the <script /> placed on <head /> (same as above).
Probably it can also happen if called with a selector that does not match any results.
Solutions:
Browser Rendering:
#yachaka snippet prevents this behavior by defining the element before placing the <Modal />:
componentWillMount() {
Modal.setAppElement('body');
}
#ungoldman answer, if you don't want to depend on `setAppElement':
Inject the bundled application JS into <body> instead of <head>.
Though ideally react-modal should wait until the DOM is loaded to try attaching to document.body.
server-side:
If rendering on server-side, you must provide a document.body, before requiring the modal script (perhaps it should be preferable to use setAppElement() in this case).
Update:
react docs have been updated to include the information above, so they should now be clearer for users running into this issue.
react-modal issue #567: add information (from issue #133 linked above) to the docs.
Just include appElement={document.getElementById('app')} inside your modal like this
<Modal
className="modal"
appElement={document.getElementById('app')}
>
It will work 100% if app is your central in index.html from where react loads.
This is my TypeScript Modal component which wraps react-modal v3.8.1:
import React from 'react'
import ReactModal from 'react-modal'
interface Props {
isOpen: boolean
ariaLabel?: string
}
const Modal: React.FC<Props> = ({
children,
ariaLabel = 'Alert Modal',
isOpen,
}) => (
<ReactModal
appElement={document.getElementById('root') as HTMLElement}
ariaHideApp={process.env.NODE_ENV !== 'test'}
isOpen={isOpen}
contentLabel={ariaLabel}
testId="modal-content"
>
{children}
</ReactModal>
)
export default Modal
Usage in component with state = { isOpen: true }:
<Modal isOpen={this.state.isOpen}>
<p>
Modal Content hereā€¦
</p>
<button onClick={() => { this.setState({ isOpen: false }) }}>Okay</button>
</Modal>
If getting the Warning: react-modal: App element is not defined... error when running tests (we were running Jest), you can suppress the warnings by adding the following to your test file:
import ReactModal from 'react-modal';
ReactModal.setAppElement('*'); // suppresses modal-related test warnings.
The shortest solution is to add
appElement={document.getElementById("hereIsYourRootElementId")}
It lets react-modal know where is your root element.
For reference, since it was a pain for me, if you are doing SSR, use the following code to prevent errors server-side:
if (typeof(window) !== 'undefined') {
ReactModal.setAppElement('body')
}
You could put this in componentDidMount() anywhere you use a modal or I put it in a custom modal component so it's nice and DRY.
Just put this
Modal.setAppElement('#root')
This will solve the warning. The root element coming from inside public folder index.html.
you need to add # before your root element id.
import React from 'react';
import Modal from 'react-modal';
Modal.setAppElement('#root');
const OptionModal = (props) => (
<Modal
isOpen={!!props.selectedOption}
contentLabel='this is the selected option'
>
<h3>Selected Option</h3>
{props.selectedOption && <p>{props.selectedOption}</p>}
<button onClick = {props.handleCloseOptionModal}>Close</button>
</Modal>
);
export default OptionModal;
here is the reference:
http://reactcommunity.org/react-modal/accessibility/
If you get that warning on testing with the "react-testing-library" here is a solution:
https://github.com/reactjs/react-modal/issues/576#issuecomment-524644035
using the react-testing-library (https://testing-library.com/) I get rid of that warning with:
import Modal from "react-modal";
const { container } = render(<MyComponent />);
Modal.setAppElement(container);
.... // to the testing, use Modal
or, if you want to test the modal component directly:
const { container, rerender } render(<MyModalComponent isOpen={false} />);
Modal.setAppElement(container);
// now the appElement is set we can show the modal component
rerender(<MyModalComponent isOpen={false} />);
.... // to the testing
For Nextjs, I think you can solve this by adding the below to outside your modal component, maybe on top, before the component is declared.
Modal.setAppElement('#__next')
Delete this attrib
className="modal"
and run again

Categories

Resources