How to test if canvas is filled with given colour? - reactjs

I have a React component which is rendering canvas element.
Inside of this component I have this method:
renderCanvas(canvas) {
canvas.fillStyle = this.props.given_colour;
canvas.fillRect(0, 0, 800, 800);
}
...which is used for creating a coloured layer.
So I have tests for calling functions in my component and for props, and they're working fine, but now I want to have a scenario checking if method above is using proper colour.
For tests I am using jest with enzyme and sinon.
I have prepared this scenario:
it("calls to fill canvas with the given colour", () => {
const canvasSpy = sinon.spy();
const component = mount(<MyCanvas given_colour="#0000FF" />);
component.instance().renderCanvas(canvasSpy);
sinon.assert.calledWith(canvasSpy, "fillStyle");
sinon.assert.calledWith(canvasSpy, "#0000FF");
sinon.restore();
});
but I am getting TypeError: canvas.fillRect is not a function
I don't know why this is happening and I am not sure about my approach to this scenario in general.
My expectations are to have a scenario which will tell me that my component, in this specific method, is using given colour. But I have no idea how to achieve that.
I'm a Rails dev and I'm new to React. Please, could someone point me what am I doing wrong with this test?

renderCanvas expects canvas to be an object that has methods, while it's a function. Assertions are wrong because canvasSpy is asserted to be called with fillStyle, while it isn't.
It likely should be:
const canvasMock = { fillRect: sinon.spy() };
component.instance().renderCanvas(canvasMock);
expect(canvasMock.fillStyle).to.be("#0000FF");
sinon.assert.calledWith(canvasMock.fillRect, 0, 0, 800, 800);

Related

Avoid re-rendering of open layers map when context changes

I am using an Open Layers map inside React. I am attaching it to the DOM using setTarget like so:
const mapObject = useMap(maplayers);
useEffect(() => {
mapObject.setTarget(mapRef.current);
return () => mapObject && mapObject.setTarget(undefined);
},[mapObject]);
Inside the useMap.js hook I am writing changes to the center and zoom of the map back to the context:
const {
zoom,
setZoom,
center,
setCenter,
} = useContext(MapContext);
let mapObject = null;
var projection = new olProj.Projection({
code: 'EPSG:25832',
units: 'm',
});
let options = {
target: 'map',
controls: olControl.defaults({
rotate: false,
zoom: false,
attributionOptions: {
collapsible: false,
},
}),
layers: maplayers,
view: new ol.View({
projection: projection,
center: center,
zoom: zoom
}),
};
mapObject = new ol.Map(options);
mapObject.on('moveend', (e) => {
console.log('moveend');
setCenter(() => e.target.getView().getCenter());
setZoom(() => e.target.getView().getZoom());
});
The problem now is that the whole map gets rerendered every time the center and zoom change. How can I avoid this? The only idea I have is to use useEffect with a dependency array that misses center and zoom but I know this is bad practise and should not be done.
You just discovered why one should not use useEffect to create Openlayers components in React. I answer this question once per week here - I will probably end up writing a very long and detailed answer and then start marking those questions as duplicates. Or maybe write a tutorial for Openlayers + React.
What you are trying to achieve is not easy and no one got it right the first time. I am the author of rlayers - https://github.com/mmomtchev/rlayers - a set of components for using Openlayers in React. The only reliable way to do everything that React expects is to use class-based components and to completely rewrite the React component life-cycle. You are free to use my components - either directly, either as an inspiration for writing your own.
In your particular case, the problem is that you create a new mapObject every time the hook is called. React detects this change and decides that the component must be rerendered. Built-in React hooks work around this by memoizing the value. Your hook should declare the dependencies of the mapObject and then use React.useMemo to always return the same object:
const mapObject = React.useMemo(() => {
// code to generate the mapObject
return mapObject;
}, [ projection, url, /* and all other items that require rerender */ ]
);
This way your hook will always return the same mapObject and you will continue to use a useEffect with only the mapObject as a dependency - according to the React guidelines.

React Testing Library - Failing to test Window Resize React Hook

I've created a custom React Hook for getting the viewport width & height on window resize (the event is debounced). The hook works fine, but I've been unable to find a way to test with React Testing Library (I keep running into errors).
I've recreated the app in CodeSandbox (along with the tests) to try to debug, but I'm running into different errors while testing.
Sometimes I get:
Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.`
But the general failures are that the data from the hook does not seem to be returned.
expect(received).toBe(expected) // Object.is equality
Expected: 500
Received: undefined
This could be something I'm missing with React Testing Library.
Any help getting to the bottom of the problem would be super appreciated!
Demo app/tests here:
https://codesandbox.io/s/useviewportsize-4l7gb?file=/src/use-viewport-size.test.tsx
===========
Solution
Thanks to #tmhao2005 it seemed that the problem was down to the hook getting the resize values from document rather than window:
setViewportSize({
width: window.innerWidth, //document.documentElement.clientWidth - doesn't work
height: window.innerHeight //document.documentElement.clientHeight - doesn't work
});
It seems that getting the clientWidth/Height is fine in the app, but fails in the React Testing Library tests.
I was opting for client sizing as I believe that does not include scollbar widths.
I think there are a few things you have to change to make your test working again:
You haven't waited to your debounce function work which is the main problem. So you can use either mock the timer or wait until your debounce function getting called.
// Make your test as `async` in case of wanting to wait
test("should return new values on window resize", async () => {
// If you go for mocking timer, uncomment this & below advance the timer
// jest.useFakeTimers();
const { result } = renderHook(() => useViewportSize());
act(() => {
window.resizeTo(500, 500);
//fireEvent(window, new Event("resize"));
});
// jest.advanceTimersByTime(251) // you can also use this way
await mockDelay(debounceDelay); // `await` 300ms to make sure the function callback run
expect(result.current.width).toBe(500);
expect(result.current.height).toBe(500);
});
You might refine your implementation code by changing to use your mock value instead:
const debouncedHandleResize = debounce(() => {
setViewportSize({
// using your mock values
width: window.innerWidth,
height: window.innerHeight
});
}, debounceTime);
PS: I also edited your codesandbox based on the async way: https://codesandbox.io/s/useviewportsize-forked-pvnc1?file=/src/use-viewport-size.test.tsx

reactjs run code after all elements are loaded

I am in my first steps with react. I am running Reactjs v16.11.0
I have page which trigger the webcam (following this mdn tutorial). So I want to call startup function when all elements were painted. I tried with window.addEventListener('load', startup, false); but it doesn't call any function.
So I tried the useEffectHook:
useEffect (() => {
startup();
}, []);
But it call the startup function too soon, and there is some elments that there aren't still in the DOM because it runs asyncronous code - video .
My startup function is this
const startup= () => {
video = document.getElementById('video');
let canvas: HTMLCanvasElement = document.getElementById('canvas') as HTMLCanvasElement;
const photo = document.getElementById('photo') as HTMLElement;
navigator.mediaDevices.getUserMedia({video: true, audio: false})
.then(function(stream) {
mediaStream = stream.getTracks()[0];
video.srcObject = stream;
video.play();
})
.catch(function(err) {
console.log("An error occurred: " + err);
});
if(video) {
video.addEventListener('canplay', function (ev: any) {
if (!streaming) {
height = video.videoHeight / (video.videoWidth / width);
// Firefox currently has a bug where the height can't be read from
// the video, so we will make assumptions if this happens.
if (isNaN(height)) {
height = width / (4 / 3);
}
video.setAttribute('width', width);
video.setAttribute('height', height);
canvas.setAttribute('width', width.toString());
canvas.setAttribute('height', height.toString());
streaming = true;
}
}, false);
//clearphoto(canvas, photo);
}
}
I am using functional component (instead of class component). And from what I understood componentDidMount works with class component. Am I correct?
How can accomplish to run the startup function only when every elements are in the DOM ?
EDIT: code edit in useEffect hook, noticed by Jayraj
I have just finished following the tutorial. It was interesting to me, as well.
First of all, you can play my demo: https://codesandbox.io/s/hungry-bassi-ojccj?file=/src/App.js.
To achieve the goal I used three hooks: useRef, useEffect, useState. Let's me explain why I have used each of them.
So, I would like to start with the useState hook. Before streaming, we should calculate the height of the image and canvas and set it. However, we must save the height into somewhere to get its value in our component. That's why I used the useState hook.
To draw canvas successfully I used the useRef hook. It allows me to access the DOM and that's why I removed calls the getElementById from the code as the hook is responsible for. I made the same with the video. I created the videoRef to access the DOM.
And the main that I called the useEffect hook two times. As you can see, the first useEffect hasn't any dependencies that's why it will be called once. It works like the componentDidMount method in this case. Thankfully to it, the getUserMedia method is called and we can set stream to the videoRef and afterwards the video will be started playing.
The second useEffect waitings for the changes of the videoRef property and then it starts executing.
I guess you should read about react hook more deeply to understand very well. Let's me attach the link of the documentation. https://reactjs.org/docs/hooks-reference.html
Have a good day.

Testing scrollintoview Jest

I have a simple function:
scrollToSecondPart() {
this.nextPartRef.current.scrollIntoView({ behavior: "smooth" });
}
and I would like to test it using Jest. When I call my function I have this error:
TypeError: Cannot read property 'scrollIntoView' of null
The code works great in the application but not in the unit test.
Here is the unit test:
it("should scroll to the second block", () => {
const scrollToSecondPart = jest.spyOn(LandingPage.prototype, 'scrollToSecondPart');
const wrapper = shallow(<LandingPage />);
const instance = wrapper.instance();
instance.scrollToSecondPart();
expect(scrollToSecondPart).toHaveBeenCalled();
});
I guess the problem is that the unit test can't access to this.nextPartRef but I don't know how I should mock this element.
By the way, I'm using "Refs" has described in https://reactjs.org/docs/refs-and-the-dom.html (I'm using React.createRef()).
Thank you!
So I stumbled on this question because I thought it was about how to test Element.scrollIntoView(), which can be done by mocking it with jest as follows:
let scrollIntoViewMock = jest.fn();
window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
<execute application code that triggers scrollIntoView>
expect(scrollIntoViewMock).toBeCalled();
However, that does not seem to be your goal. In your test you are calling scrollToSecondPart() in your test and then expect(scrollToSecondPart).toHaveBeenCalled() which is essentially testing that scrollToSecondPart is a function of LandingPage or am I missing something?
for me i had to mock the scrollIntoView for elements like this:
const scrollIntoViewMock = jest.fn();
Element.prototype.scrollIntoView = scrollIntoViewMock()
as found here:
https://github.com/jsdom/jsdom/issues/1695#issuecomment-449931788

Simulate a custom Event using Jest and Enzyme

class AgGridWrapper extends React.Component {
render() {
return <AgGridReact onCellClicked={this.onCellClicked.this(bind)}
}
onCellClicked= (event) = > {
event.setDataValue(newValue);
}
}
I have a wrapper component AgGridWrapper around AgGridReact (AgGridReact is a node module which helps to create HTML Grids). In this example when i click on any cell on the AgGrid it will change the value of the cell.
I want to test this onCellClicked event. so i did the following.
const wrapper= shallow(<AgGridWrapper />);
const agGridReact = wrapper.find(AgGridReact);
const eventObject={};
agGridReact.simulate('cellClicked',eventObect);
My intention is to simulate the cellClicked event and check whether the value of the cell got changed or not.
But here i dont have access to the agGridReact's setDataValue() method.
so i am not able to mock this eventObject.
I got stecked here for a couple of days now. and i am under the impression that its technically not possible to test this functionality because setDataValue is not accessible to us.
Any Suggestions on this??
As simulate does nothing different than just call a function that lives under the on{propName}. For some reason this only works for the standard events but in the end there is no different than call it by yourself:
const eventObject = {setDataValue: jest.fn()}
gGridReact.prop('cellClicked')(eventObect);
expect(eventObject).toHaveBeenCalled()

Resources