I am testing a react hook which gets the window-dimensions from window.innerheight and window.innerwidth. It uses the following code to handle server side rendering:
...
typeof window=="undefined" ? {width: 1024, height: 768} : functionToGetDimensions()
...
When trying to make tests for this line in react-testing-library i need to delete the window object:
...
const TestComponent = ({ }) => {
const { width, height } = useWindowDimensions();
return (
<div>
<p data-testid="width">{width}</p>
<p data-testid="height">{height}</p>
</div>)
}
describe('when window is undefined', () => {
//#ts-ignore
const testWidth = 1000;
const testHeight = 800;
const fallbackWidth = 1024; // when useWindowdimensions detects window = undefined
const { window } = global;
beforeAll(() => {
// #ts-ignore
delete global.window;
});
afterAll(() => {
global.window = window;
});
it('runs without error', () => {
render(<TestComponent />)
expect(screen.queryByTestId("height")).toHaveTextContent(fallbackWidth );
});
});
I get the following error:
ReferenceError: window is not defined
I've tried using node as test enviroment, but then document is not defined, and render does not work.
Related
When i try to manipulate iframe i always get the cors issue permission denied to access property document on cross-origin, same goes when i try to attach event listener.
So is it possible to manipulate DOM elements inside iframe that is coming from different origin?
I'm happy to somehow also take a copy of iframe and just have it in my code as long as it allows me to edit it later, but unsure how?
import { useEffect } from 'react';
const post = () => {
const iframe = document.querySelector('iframe');
const message = { type: 'modifyDOM', text: 'Hello World!' };
// window.postMessage(message);
if (iframe?.contentWindow) {
iframe.contentWindow.postMessage(message, 'http://example.com');
}
};
export const IFrame = () => {
useEffect(() => {
const iframe = document.querySelector('iframe');
const receiveMessage = (e: MessageEvent) => {
if (e.data.type === 'modifyDOM') {
console.log(e.data);
// window.document.body.innerHTML = '<h1>Hello World!</h1>';
if (iframe?.contentWindow) {
iframe.contentWindow.document.body.innerHTML = '<h1>Hello World!</h1>';
}
}
};
window.addEventListener('message', receiveMessage);
return () => {
window.removeEventListener('message', receiveMessage);
};
}, []);
return (
<div>
<button onClick={post}>Post</button>
<iframe src="http://example.com" />
</div>
);
};
Following the Mapbox draw example I can use the draw variable to access all features that are drawn on a map.
const draw = new MapboxDraw({
// ...
});
map.addControl(draw);
// ...
function updateArea(e) {
const data = draw.getAll(); // Accessing all features (data) drawn here
// ...
}
However, in react-map-gl library useControl example I can not figure out how to pass ref to the DrawControl component so I can use it as something like draw.current in a similar way as I did draw in normal javascript above.
In my DrawControl.jsx
const DrawControl = (props) => {
useControl(
({ map }) => {
map.on('draw.create', props.onCreate);
// ...
return new MapboxDraw(props);
},
({ map }) => {
map.off('draw.create', props.onCreate);
// ...
},{
position: props.position,
},
);
return null;
};
In my MapDrawer.jsx
import Map from 'react-map-gl';
import DrawControl from './DrawControl';
// ...
export const MapDrawer = () => {
const draw = React.useRef(null);
const onUpdate = React.useCallback((e) => {
const data = draw.current.getAll(); // this does not work as expected
// ...
}, []);
return (
// ...
<Map ...>
<DrawControl
ref={draw}
onCreate={onUpdate}
onUpdate={onUpdate}
...
/>
</Map>
)
}
I also get an error stating I should use forwardRef but I'm not really sure how.
react_devtools_backend.js:3973 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
What I need is basically to delete the previous feature if there is a new polygon drawn on a map so that only one polygon is allowed on a map. I want to be able to do something like this in the onUpdate callback.
const onUpdate = React.useCallback((e) => {
// ...
draw.current.delete(draw.current.getAll.features[0].id);
// ...
}, []);
I had the similar problem recently with that lib, I solved it doing the following :
export let drawRef = null;
export default const DrawControl = (props) => {
drawRef = useControl(
({ map }) => {
map.on('draw.create', props.onCreate);
// ...
return new MapboxDraw(props);
},
({ map }) => {
map.off('draw.create', props.onCreate);
// ...
},{
position: props.position,
},
);
return null;
};
import DrawControl, {drawRef} from './DrawControl';
// ...
export const MapDrawer = () => {
const draw = drawRef;
const onUpdate = React.useCallback((e) => {
const data = draw?draw.current.getAll():null; // this does not work as expected
// ...
}, []);
return (
// ...
<Map ...>
<DrawControl
onCreate={onUpdate}
onUpdate={onUpdate}
...
/>
</Map>
)
}
const onUpdate = React.useCallback((e) => {
// ...
drawRef.delete(drawRef.getAll.features[0].id);
// ...
}, []);
Once component created, the ref is available for use.
Not that elegant but working... Sure there might be cleaner way...
Hope that helps!
Cheers
Pass draw from draw control to parent component.
const DrawControl = (props) => {
const [draw, setDraw] = useState()
const { setDraw: setDrawInParent, onUpdate, onCreate, onDelete } = props;
useEffect(() => {
if (draw) setDrawInParent(draw)
}, [draw])
useControl(
({ map }) => {
map.on("draw.create", onCreate);
map.on("draw.update", onUpdate);
map.on("draw.delete", onDelete);
const draw = new MapboxDraw(props);
setDraw(draw);
return draw;
}
);
return null;
};
I think I found a better solution combine forwardRef and useImperativeHandle to solve:
export const DrawControl = React.forwardRef((props: DrawControlProps, ref) => {
const drawRef = useControl<MapboxDraw>(
() => new MapboxDraw(props),
({ map }) => {
map.on("draw.create", props.onCreate);
map.on("draw.update", props.onUpdate);
map.on("draw.delete", props.onDelete);
map.on("draw.modechange", props.onModeChange);
},
({ map }) => {
map.off("draw.create", props.onCreate);
map.off("draw.update", props.onUpdate);
map.off("draw.delete", props.onDelete);
map.off("draw.modechange", props.onModeChange);
},
{
position: props.position,
}
);
React.useImperativeHandle(ref, () => drawRef, [drawRef]); // This way I exposed drawRef outside the component
return null;
});
in the component:
const drawRef = React.useRef<MapboxDraw>();
const [drawMode, setDrawMode] = React.useState<DrawMode>(“draw_polygon");
const changeModeTo = (mode: DrawMode) => {
// If you programmatically invoke a function in the Draw API, any event that directly corresponds with that function will not be fired
drawRef.current?.changeMode(mode as string);
setDrawMode(mode);
};
<>
<DrawControl
ref={drawRef}
position="top-right”
displayControlsDefault={false}
controls={{
polygon: true,
trash: true,
}}
defaultMode=“draw_polygon"
onCreate={onUpdate}
onUpdate={onUpdate}
onDelete={onDelete}
onModeChange={onModeChange}
/>
<button
style={{
position: ‘absolute’,
left: ‘20px’,
top: ‘20px’,
backgroundColor: '#ff0000’,
}}
onClick={() => changeModeTo('simple_select’)}
>
Change to Simple Select
</button>
<>
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"));
I am learning to test React app using jest and Enzyme . I have created a component and using redux to maintain and update the state . The component code is below .
Now i want to write the test to check initial value of prodOverviewAccordion which we are setting as true in context file.
I have tried writing , but getting error . Sharing the test code also . Please help
const ProdOverview = () => {
const {
productState,
setProdOverviewAccordion
} = React.useContext(ProductContext);
const {prodOverviewAccordion } = productState;
const [completeStatusprod, setCompleteStatusprod] = useState(false);
return (
<div onClick={toggleTriggerProd}>
<s-box>
<Collapsible
trigger={
<Accordion
name={ProductConfig.accordionTriggerLabels.prodOverviewLabel}
completeStatusIcon={completeStatusprod ? 'check-circle' : 'alert-triangle'}
completeStatus={completeStatusprod}
/>
}
easing='ease-out'
handleTriggerClick={() => {
if (!prodOverviewAccordion) {
setProdOverviewAccordion(true);
} else {
setProdOverviewAccordion(false);
}
}}
open={prodOverviewAccordion}
data-test='prodOverViewCollapsible'
>
<p>Test</p>
</Collapsible>
</s-box>
</div>
);
};
export default ProdOverview;
const prodsetup = (props = {}) => {
return shallow(<ProdOverview />);
};
describe('Product Overview panel Test', () => {
const mockSetCurrentGuess = jest.fn();
beforeEach(() => {
mockSetCurrentGuess.mockClear();
});
test('should render Collapsible panel', () => {
const wrapper = prodsetup();
const component = findByTestAttr(wrapper, 'prodOverViewCollapsible');
expect(component.length).toBe(1);
});
test('Product Overview Panel should be in open state', () => {
const wrapper = prodsetup();
expect(wrapper.state().prodOverviewAccordion.to.equal(true));
});
});
I have a React Native component that makes an Image.getSize request for each image in the component. Then within the callback of the Image.getSize requests, I set some state for my component. That all works fine, but the problem is that it's possible for a user to transition away from the screen the component is used on before one or more Image.getSize requests respond, which then causes the "no-op memory leak" error to pop up because I'm trying to change state after the component has been unmounted.
So my question is: How can I stop the Image.getSize request from trying to modify state after the component is unmounted? Here's a simplified version of my component code. Thank you.
const imgWidth = 300; // Not actually static in the component, but doesn't matter.
const SomeComponent = (props) => {
const [arr, setArr] = useState(props.someData);
const setImgDimens = (arr) => {
arr.forEach((arrItem, i) => {
if (arrItem.imgPath) {
const uri = `/path/to/${arrItem.imgPath}`;
Image.getSize(uri, (width, height) => {
setArr((currArr) => {
const newWidth = imgWidth;
const ratio = newWidth / width;
const newHeight = ratio * height;
currArr = currArr.map((arrItem, idx) => {
if (idx === i) {
arrItem.width = newWidth;
arrItem.height = newHeight;
}
return arrItem;
});
return currArr;
});
});
}
});
};
useEffect(() => {
setImgDimens(arr);
return () => {
// Do I need to do something here?!
};
}, []);
return (
<FlatList
data={arr}
keyExtractor={(arrItem) => arrItem.id.toString()}
renderItem={({ item }) => {
return (
<View>
{ item.imgPath ?
<Image
source={{ uri: `/path/to/${arrItem.imgPath}` }}
/>
:
null
}
</View>
);
}}
/>
);
};
export default SomeComponent;
I had to implement something similar, I start by initialising a variable called isMounted.
It sets to true when the component mounts and false to when the component will unmount.
Before calling setImgDimens there's a check to see if the component is mounted. If not, it won't call the function and thus will not update state.
const SomeComponent = (props) => {
const isMounted = React.createRef(null);
useEffect(() => {
// Component has mounted
isMounted.current = true;
if(isMounted.current) {
setImgDimens(arr);
}
return () => {
// Component will unmount
isMounted.current = false;
}
}, []);
}
Edit: This is the answer that worked for me, but for what it's worth, I had to move the isMounted variable to outside the SomeComponent function for it to work. Also, you can just use a regular variable instead of createRef to create a reference, etc.
Basically, the following worked for me:
let isMounted;
const SomeComponent = (props) => {
const setImgDimens = (arr) => {
arr.forEach((arrItem, i) => {
if (arrItem.imgPath) {
const uri = `/path/to/${arrItem.imgPath}`;
Image.getSize(uri, (width, height) => {
if (isMounted) { // Added this check.
setArr((currArr) => {
const newWidth = imgWidth;
const ratio = newWidth / width;
const newHeight = ratio * height;
currArr = currArr.map((arrItem, idx) => {
if (idx === i) {
arrItem.width = newWidth;
arrItem.height = newHeight;
}
return arrItem;
});
return currArr;
});
}
});
}
});
};
useEffect(() => {
isMounted = true;
setImgDimens(arr);
return () => {
isMounted = false;
}
}, []);
};