Jest / Enzyme wait for the element to have a specific attribute - reactjs

I'm creating a weather app with React and testing it with enzyme, on each page reload and on the initial page load the background image is supposed to change/load
App.js
function App() {
const [background, setBackground] = useState('');
async function fetchBgImage() {
// getRandomImage returns a link fetched from the api
setBackground(await getRandomImage());
}
useEffect(() => {
fetchBgImage(); //a method which sets the background image and sets background to image url
}, []);
return (
<div className="App">
// Here backgound is just an image url
<img id="background_image" src={background} alt="background image" />
</div>
);
}
In the real world the app works fine and the background image load properly and changes on each page reload, but when I'm testing with enzyme, it doesn't wait for the backgound property to be set and, consequently, the src property is empty.
App.test.js
beforeEach(() => {
// I read that useEffect doesnt work with shallow rendering
// https://github.com/airbnb/enzyme/issues/2086
jest.spyOn(React, 'useEffect').mockImplementation((f) => f());
});
it('displays background image', () => {
const wrapper = shallow(<App />);
const image = wrapper.find('#background_image');
// src prop is empty
console.log(image.props());
});
So how do I make enzyme wait for the src property of the image to be set?

You could write a helper function to wait for the attribute. Something like:
function waitForAttribute(el, attribute) {
return new Promise((resolve, reject) => {
const tryInterval = 100; // try every 100ms
let maxTries = 10; //try 10 times
let currentTry = 0;
const timer = setInterval(() => {
if (currentTry >= maxTries) {
clearInterval(timer);
return reject(new Error(`${attribute} not found on ${el.debug()}`));
}
const prop = el.props()[attribute]
if (prop) {
clearInterval(timer);
resolve(el);
}
currentTry++
}, tryInterval);
});
}
it('displays background image', async () => {
const wrapper = shallow(<App />);
const image = wrapper.find('#background_image');
const srcValue = await waitForAttribute(image, 'src')
// ...do something with it
});

Related

Memory leak when a iframe with url pointing to a react app is removed and added

I have a react-redux app which runs inside iframe, Iframe with same exact url is getting removed and added back by the parent application but doing so is causing memory footprint and CPU usage growing as we keep doing more number of times (reaches 5 GB after trying around 100 times), I believe it may not be something related to just react but in general as well, any help would be much appreciated.
React App in iframe:
const postMessage = () => {
window.parent.postMessage(JSON.stringify({ type: "TYPEB", message: "some message to parent" }), location.origin);
}
const messageEventHandler = (event) => {
//event handling code
}
const addMessageEventListenerFromParent = () => {
window.addEventListener("message", messageEventHandler);
};
const rootComponent = () =>
useEffect(() => {
// adding event listeners
addMessageEventListenerFromParent();
postMessage();
return () => {
console.log("unmounting the app");
removeMessageEventListenerFromParent();
};
}, []);
}
Parent App (React app):
const Iframe = (props) => {
const iframeRef = useRef(null);
const postMessage = (message) => {
if (iframeEl?.current.contentWindow?.postMessage) {
iframeEl.current.contentWindow.postMessage(message, location.origin);
}
};
useEffect(() => {
postMessage({
type: "TYPEA",
payload: somePayload,
})
}, [props.propA])
const memoizedIframe = useMemo(() => (
<iframe ref= { iframeRef } key = { key } className = "iframetask" src = { sourceUri } />
), [inputs]);
return memoizedIframe;
}
export default memo(Iframe);
This is a sample code of my app (running in iframe), return function (cleanup function) not getting executed when the iframe is removed and added back by parent app, this means the component is not getting unmounted but getting mounted as iframe is re-inserted.

Testing a window event addEventListener with a callback using enzyme, mocha and chai

I am having trouble testing the callback in my window addEventListener that's wrapped in a useEffect. I am unable to get coverage for the callback setHighRes and also the
...
return () => {
window.removeEventListener("ATF_DONE", setHighRes);
};
JSX file
// Checks to see if image is cached
const isCached = src => {
const img = new Image(); // eslint-disable-line
img.src = src;
const complete = img.complete;
img.src = "";
return complete;
};
const [isHighRes, lazySetIsHighRes] = useState(
!isCached(`${images[0].normal}?wid=200&hei=200`)
);
useEffect(() => {
// If image is cached load the high res image after the ATF_DONE event
if (!isHighRes) {
const setHighRes = () => {
lazySetIsHighRes(true);
window.removeEventListener("ATF_DONE", setHighRes);
};
window.setTimeout(() => {
window.addEventListener("ATF_DONE", setHighRes);
return () => {
window.removeEventListener("ATF_DONE", setHighRes);
};
}, ATF_TIMEOUT)
}
return null;
}, []);
I tried doing this in the spec.jsx but it fails because expected addEventListener to have been called with arguments
beforeEach(() => {
sandbox = sinon.sandbox.create();
sinon.stub(window, "addEventListener");
sinon.stub(window, "removeEventListener");
sandbox.stub(window, "Image").callsFake(() => {
const image = new Image.wrappedMethod();
sandbox.stub(image, "complete").value(complete);
return image;
});
it.only("should load low res image if image is cached", () => {
const clock = sinon.useFakeTimers();
clock.tick(8000);
expect(window.addEventListener).to.have.been.calledWith("ATF_DONE", "setHighRes");
})
Looks like you can manually dispatch the event to trigger the callback which worked for me
window.dispatchEvent(new Event("ATF_DONE"));

How to test a component with updates background depend on state changes

I have a component that changes the background image depending on the state. I added simplified codes down below.
Since I fetch an image from the server on state changes, the background image was flashing. This is the reason I load them to DOM with preloadImage() function. This function solved the issue.
The problem starts with testing. See the testing file!
const BackgroundImage = styled`
...
background-image: ${(props) => props.bg && `url(${props.bg})`};
`
const preloadImage = (src, wrapperRef, callback) => {
const img = new Image();
img.src = src;
img.style.display = 'none';
img.dataset.testid = 'preloaded-image';
const el = wrapperRef.current;
el.innerHTML = '';
el.appendChild(img);
img.onload = () => typeof callback === 'function' && callback(src);
};
const Panel = (defaultBG) => {
const imageCacheRef = useRef();
const [bg, setBG] = useState(defaultBG);
useEffect(() => {
const fetchImage = async () => {
const imageSrc = await import(`https://fakeimageapi.com/${bg}.png`);
return preloadImage(imageSrc.default, imageCacheRef, setImage);
}
try {
await fetchImage()
} catch (error) {
console.log(error)
}
}, [])
return (
<div ref={imageCacheRef}>
<BackgroundImage bg={bg} data-testid="bg" />
<button onClick={ () => setBG('cat') }>Cat</button>
<button onClick={ () => setBG('dog') }>Cat</button>
<button onClick={ () => setBG('rabbit') }>Cat</button>
<button onClick={ () => setBG('parrot') }>Cat</button>
</div>
)
}
This is the test suite written with Testing Library.
import { render, waitFor, screen, act } from '#testing-library/react';
describe('Panel', () => {
test('Sets background-image correctly', async () => {
render(<Panel defaultBG="panda" />)
expect(screen.getByTestId('bg')).toHaveStyle(
'background-image: url(panda.png);',
);
})
})
Unfortunately, this test fails. The problem (I guess) that I use a callback after the image is loaded inside useEffect. How can I final this test with a successful result?
The problem is solved. I added a test-id to the image inside preloadImage() and loaded the image with the fireEvent method. That's it!
import { render, waitFor, screen, fireEvent } from '#testing-library/react';
describe('Panel', () => {
test('Sets background-image correctly', async () => {
render(<Panel defaultBG="panda" />)
const image = await waitFor(() => screen.getByTestId('preloaded-image'));
fireEvent.load(image);
expect(screen.getByTestId('bg')).toHaveStyle(
'background-image: url(panda.png);',
);
})
})
Also, some refactoring on preloadImage() function.
const preloadImage = (src, wrapperRef, callback) => {
const img = new Image();
img.src = src;
img.style.display = 'none';
img.dataset.testid = 'preloaded-image';
const el = wrapperRef.current;
el.innerHTML = '';
el.appendChild(img);
if (typeof callback === 'function') {
img.onload = () => callback(src);
}
};

React/reselect how to refresh ui before a selector is recomputed

In my React-App (create-react-app) I use a selector (created with reselect) to compute derived data from stored data.The Problem is, the selector takes a long time to compute. I would like to show a spinner (or a message) on the user interface. But each time the selector is recomputed the ui freezes.
I read a lot of stuff (Web Worker, requestIdleCallback, requestAnimationFrame) and try to make my own React hook but I am stuck. I cannot use the selector in callbacks.
The searched result is simply to get the ui refreshed before the selector is recomputed.
That's my solution. I don't know if it's "good" or if it breaks some rules of react or reselect, but it works. Maybe you can assess that?The code is simplified to improve readability.
The idea is, the selector returns a Promise and I call requestAnimationFrame before computing the data to get a chance to refresh the ui.
selector.js:
const dataSelector = state => state.data;
export const getDataComputedPromise = createSelector([dataSelector], (data) => {
const compute = function () {
return new Promise((resolve) => {
// heavy computing stuff
resolve(computedData);
});
};
return new Promise((resolve) => {
let start = null;
let requestId = null;
function step (timestamp) {
if (!start) {
start = timestamp;
window.cancelAnimationFrame(requestId);
requestId = window.requestAnimationFrame(step);
return;
};
compute().then(freeResources => {
window.cancelAnimationFrame(requestId);
resolve(freeResources);
});
}
requestId = window.requestAnimationFrame(step);
});
});
myPage.js
const MyPage = ({ someProps }) => {
...
const dataComputedPromise = useSelector(getDataComputedPromise);
const [dataComputed, setDataComputed] = useState([]);
const [computeSelector, setComputeSelector] = useState(false);
useEffect(() => {
setComputeSelector(true);
}, [data]);
useEffect(() => {
dataComputedPromise.then(dataComputed => {
setDataComputed(dataComputed);
setComputeSelector(false);
});
}, [dataComputedPromise]);
...
return <div>
<Loader active={compueSelector}>Computing data...</Loader>
</div>;
};
export default MyPage;

React Hooks How to get to componentWillUnmount

Hello I'm trying to pass the following code to reacthooks:
import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';
class SomeComponent extends React.Component {
// 2. Initialise your ref and targetElement here
targetRef = React.createRef();
targetElement = null;
componentDidMount() {
// 3. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav).
// Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element).
// This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired.
this.targetElement = this.targetRef.current;
}
showTargetElement = () => {
// ... some logic to show target element
// 4. Disable body scroll
disableBodyScroll(this.targetElement);
};
hideTargetElement = () => {
// ... some logic to hide target element
// 5. Re-enable body scroll
enableBodyScroll(this.targetElement);
}
componentWillUnmount() {
// 5. Useful if we have called disableBodyScroll for multiple target elements,
// and we just want a kill-switch to undo all that.
// OR useful for if the `hideTargetElement()` function got circumvented eg. visitor
// clicks a link which takes him/her to a different page within the app.
clearAllBodyScrollLocks();
}
render() {
return (
// 6. Pass your ref with the reference to the targetElement to SomeOtherComponent
<SomeOtherComponent ref={this.targetRef}>
some JSX to go here
</SomeOtherComponent>
);
}
}
And then I did the following with hooks:
const [modalIsOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
const targetRef = useRef();
const showTargetElement = () => {
disableBodyScroll(targetRef);
};
const hideTargetElement = () => {
enableBodyScroll(targetRef);
};
useEffect(() => {
if (modalIsOpen === true) {
showTargetElement();
} else {
hideTargetElement();
}
}, [modalIsOpen]);
I don't know if I did it correctly with useRef and useEffect, but it worked, but I can't imagine how I'm going to get to my componentWillUnmount to call mine:
clearAllBodyScrollLocks ();
The basic equivalents for componentDidMount and componentWillUnmount in React Hooks are:
//componentDidMount
useEffect(() => {
doSomethingOnMount();
}, [])
//componentWillUnmount
useEffect(() => {
return () => {
doSomethingOnUnmount();
}
}, [])
These can also be combined into one useEffect:
useEffect(() => {
doSomethingOnMount();
return () => {
doSomethingOnUnmount();
}
}, [])
This process is called effect clean up, you can read more from the documentation.

Resources