Trying to set up Jest to test my React components (Technically I'm using Preact) but same idea...
Anytime I try to get a coverage report, I get errors when it hits any jsx syntax.
Running coverage on untested files...Failed to collect coverage from /index.js
ERROR: /index.js: Unexpected token (52:2)
50 |
51 | render(
> 52 | <Gallery images={images} />,
| ^
I've tried following the docs and similar issues but no luck! It seems as though my babel settings aren't getting used by Jest.
Any idea how to get rid of the error?
{
"name": "tests",
"version": "1.0.0",
"description": "",
"main": "Gallery.js",
"scripts": {
"test": "jest --coverage",
"start": "parcel index.html"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.5.0",
"@babel/plugin-proposal-class-properties": "^7.5.0",
"@babel/plugin-proposal-export-default-from": "^7.5.2",
"@babel/plugin-transform-runtime": "^7.5.0",
"@babel/preset-env": "^7.4.5",
"@babel/preset-react": "^7.0.0",
"babel-jest": "^24.8.0",
"babel-plugin-transform-export-extensions": "^6.22.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"enzyme": "^3.10.0",
"jest": "^24.8.0",
"jest-cli": "^24.8.0",
"parcel-bundler": "^1.12.3",
"react-test-renderer": "^16.8.6"
},
"dependencies": {
"preact": "^8.4.2"
},
"jest": {
"verbose": true,
"transform": {
"^.+\\.jsx?$": "<rootDir>/node_modules/babel-jest"
},
"collectCoverageFrom": [
"**/*.{js,jsx}",
"!**/node_modules/**"
]
}
}
{
"presets": [
[
"@babel/preset-env", {
"targets": {
"node": "current"
}
},
"@babel/preset-react"
]
],
"plugins": [
["@babel/plugin-transform-runtime", {
"regenerator": true
}],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-export-default-from",
"babel-plugin-transform-export-extensions"
]
}
My component is loaded into my index.js
file like so:
import { h, render } from 'preact';
import Gallery from './Gallery'
import "./gallery.css"
const images = [ ... /* Some object in here */ ];
render(
<Gallery images={images} />,
document.getElementById('test'),
);
/** @jsx h */
import { h, Component } from 'preact';
export default class Gallery extends Component {
constructor(props) {
super(props);
// Set initial state
this.state = {
showLightbox: false,
};
}
// Handle Keydown function with event parameter
handleKeyDown = (event) => {
const { showLightbox } = this.state;
// If the lightbox is showing
if (showLightbox) {
// Define buttons and keycodes
const firstArrow = document.querySelector('.lightbox .arrows .arrows__left');
const lastArrow = document.querySelector('.lightbox .arrows .arrows__right');
const closeIcon = document.querySelector('.lightbox .close-button');
const TAB_KEY = 9;
const ESCAPE_KEY = 27;
const LEFT_ARROW = 37;
const RIGHT_ARROW = 39;
// If esc is clicked, call the close function
if (event.keyCode === ESCAPE_KEY) this.onClose();
// If left arrow is clicked, call the changeImage function
if (event.keyCode === LEFT_ARROW) this.changeImage(event, -1);
// If left arrow is clicked, call the changeImage function
if (event.keyCode === RIGHT_ARROW) this.changeImage(event, 1);
// If tab is clicked, keep focus on the arrows
if (event.keyCode === TAB_KEY && !event.shiftKey) {
if (document.activeElement === firstArrow) {
event.preventDefault();
lastArrow.focus();
} else if (document.activeElement === lastArrow) {
event.preventDefault();
closeIcon.focus();
} else {
event.preventDefault();
firstArrow.focus();
}
}
if (event.keyCode === TAB_KEY && event.shiftKey) {
if (document.activeElement === firstArrow) {
event.preventDefault();
closeIcon.focus();
} else if (document.activeElement === lastArrow) {
event.preventDefault();
firstArrow.focus();
} else {
event.preventDefault();
lastArrow.focus();
}
}
}
}
// onClick function
onClick = (e, key) => {
// Prevent default action (href="#")
e.preventDefault();
/*
Set state:
activeImage = the image's index in the array of images
showLightbox = true
Callback:
- Get left arrow button and focus on it
- Add no scroll class to body
- Call scrollToThumb function
*/
this.setState({
activeImage: key,
showLightbox: true,
}, () => {
document.querySelector('.lightbox .arrows .arrows__left').focus();
document.body.classList.add('no-scroll');
this.scrollToThumb();
});
}
// onClose function
onClose = () => {
/*
Set state:
showLightbox = false
Callback:
- Remove no scroll class from body
*/
this.setState({
showLightbox: false,
}, () => document.body.classList.remove('no-scroll'));
}
// / changeImage function
changeImage = (e, calc) => {
const { activeImage } = this.state;
const { images } = this.props;
let newCalc = calc;
// If first image is active and parameter is -1
if (activeImage === 0 && calc === -1) {
// set parameter to the length of the array to go right to the last image
newCalc = images.length - 1;
} else if (activeImage === (images.length - 1) && calc === 1) {
// If last image is active and parameter is 1
// set parameter to the (negative)length of the array to go right to the first image
newCalc = -(images.length - 1);
}
/*
Set state:
activeImage = selected image + or - calc amount
Callback:
- Call scrollToThumb function
*/
this.setState(state => ({
activeImage: state.activeImage + newCalc,
}), () => this.scrollToThumb());
}
// scrollToThumb function
scrollToThumb = () => {
/* Define variables for:
- Lightbox div
- Thumbs div
- First thumbnail div
- Active thumbnail div
- The offsetTop of the clicked thumbnail on mobile devices
- X-axis offset of first div
*/
const lightbox = document.querySelector('.lightbox');
const thumbs = document.querySelector('.thumbs');
const firstThumb = document.querySelectorAll('.thumb')[0];
const activeThumb = document.querySelector('.thumb--active');
const activeTop = document.querySelector('.thumb--active').offsetTop;
const firstOffset = firstThumb.offsetLeft;
// Set the scroll position to show the selected thumb with some space to the left (200px)
thumbs.scrollLeft = activeThumb.offsetLeft - firstOffset - 200;
// Set the scroll top to scroll to pressed thumbnail image for mobile devices
lightbox.scrollTop = activeTop - 30;
}
/*
renderOverlay function
Parameters:
- maxImages = based on the layout prop, how many images are the maximum that will show on page
- i = the current image number
*/
renderOverlay = (maxImages, i) => {
const { images } = this.props;
// Set overflow images to the amount of EXTRA images not showing on page
const overflowImages = images.length - maxImages;
// plural Or No is set to "s" if there is more than one and blank if there is just one
const pluralOrNo = overflowImages > 1 ? 's' : '';
// If there are more images than the max amount showing AND it is the last image
if (images.length > maxImages && i === maxImages) {
// Return an overlay with an extra class and content showing the amount of images left
return (
<div className="gallery-image__overlay gallery-image__overlay--last">
{`+${overflowImages} more image${pluralOrNo}`}
</div>
);
}
// Otherwise...
// Return the blank overlay
return <div className="gallery-image__overlay" />;
}
/*
galleryImage function
Parameters:
- cols = Chassis columns defined based on the selected style and which image it is
- path = image.path
- alt = image.alt
- i = image number
*/
galleryImage = (cols, path, alt, maxImages, i) => (
<div className={cols}>
<a
onClick={e => this.onClick(e, i)}
href="#lightbox"
>
<div className="gallery-image">
<img
src={path}
alt={alt}
className="ch-img--responsive ch-hand gallery-image__image"
/>
{this.renderOverlay(maxImages, (i + 1))}
</div>
</a>
</div>
)
// renderImages function
renderImages = () => {
let cols;
let maxImages;
const { layout, images } = this.props;
if (layout === '4/3') {
maxImages = 7;
} else if (layout === '4') {
maxImages = 4;
} else if (layout === '6') {
maxImages = 6;
} else {
maxImages = layout === '4/3' ? 7 : 8;
}
// Cleaned images array is the first 7 images
const cleanedImages = images.slice(0, maxImages);
// Amount is the length of that array (I've done this incase we change 7 to a different number)
const amount = cleanedImages.length;
// Map the images
const returnImages = cleanedImages.map((image, i) => {
// If the defined style is four by 3...
if (layout === '4/3') {
// Layout for the second and third-last image
if ((amount - 1) === i + 1 || (amount - 2) === i + 1) cols = 'xs:ch-col--6 sm:ch-col--4 ch-mb--2 sm:ch-mb--4';
// Layout for the last image
else if (amount === i + 1) cols = 'xs:ch-col--12 sm:ch-col--4 ch-mb--2 sm:ch-mb--4';
// Otherwise, layout is just a simple grid
else cols = 'xs:ch-col--6 sm:ch-col--3 ch-mb--2 sm:ch-mb--4';
} else if (layout === '6') {
// If the defined style is four by 3...
// Layout is just a simple grid
cols = 'xs:ch-col--6 sm:ch-col--4 ch-mb--2 sm:ch-mb--4';
} else cols = 'xs:ch-col--6 sm:ch-col--3 ch-mb--2 sm:ch-mb--4';
// Return an image from the galleryImage function based on the parameters from above
return (
this.galleryImage(cols, image.path, image.alt, maxImages, i)
);
});
// Return images
return returnImages;
}
// renderLightbox function
renderLightbox = () => {
const showLightbox = this.state;
// Listen for keydown event and call function
document.addEventListener('keydown', this.handleKeyDown);
// Render lightbox
const lightbox = (
<div
className={`lightbox ${showLightbox ? 'lightbox--visible' : ''}`}
>
{this.renderImage()}
{this.renderCounter()}
<div className="thumbs ch-mh--auto">
{this.renderThumbnails()}
</div>
<button
className="ch-pull--right close-button ch-ma--3"
onClick={e => this.onClose(e)}
type="button"
/>
</div>
);
return lightbox;
}
// renderImage function to show featuredImage
renderImage = () => {
const { images } = this.props;
const { activeImage } = this.state;
return (
<div className="ch-display--none md:ch-display--flex imageContainer">
<figure>
<div className="overlays ch-mh--auto md:ch-mt--8 ch-hand">
<div
className="overlay"
onClick={e => this.changeImage(e, -1)}
/>
<div
className="overlay"
onClick={e => this.changeImage(e, 1)}
/>
</div>
<img
src={images[activeImage].path}
alt={images[activeImage].alt}
className="ch-img--responsive featuredImage ch-mh--auto md:ch-mt--8 ch-hand"
onClick={e => this.changeImage(e, 1)}
/>
<figcaption className="caption ch-mt--1 ch-mh--auto ch-mb--4 ch-text--center">{images[activeImage].caption}</figcaption>
</figure>
{this.renderNavigation()}
</div>
);
}
// renderCounter function to show which image the user is on
renderCounter = () => {
const { images } = this.props;
const { activeImage } = this.state;
return (
<p className="counter ch-display--none md:ch-display--block ch-text--center ch-mb--0">
{`Image ${activeImage + 1}/${images.length}`}
</p>
);
}
// renderNavigation function to show arrows
renderNavigation = () => (
<div className="arrows ch-display--none md:ch-display--block">
<button
className="arrow arrows__left ch-absolute"
onClick={e => this.changeImage(e, -1)}
type="button"
/>
<button
className="arrow arrows__right ch-absolute"
onClick={e => this.changeImage(e, 1)}
type="button"
/>
</div>
)
// renderThumbnails function to show list of thumbnails (On mobile these will be used)
renderThumbnails = () => {
const { images } = this.props;
const { activeImage } = this.state;
const thumbs = images.map((image, i) => (
<div
className={`thumb md:ch-display--inline-block ch-mt--4 md:ch-mt--2 ch-mr--2${i === activeImage ? ' thumb--active md:ch-ba--2 md:ch-bc--white' : ''}`}
onClick={e => this.onClick(e, i)}
>
<figure>
<img
src={images[i].path}
alt={images[i].alt}
className="ch-img--responsive ch-mh--auto ch-mt--4 md:ch-mt--0"
/>
<figcaption className="caption ch-mt--1 ch-mh--auto ch-mb--4 md:ch-mb--8 md:ch-display--none">{images[i].caption}</figcaption>
</figure>
</div>
));
return thumbs;
}
// Final render function
render() {
const { showLightbox } = this.state;
return (
<div>
{this.renderImages()}
{showLightbox ? this.renderLightbox() : null}
</div>
);
}
}
Had the same problem. Renaming the .babelrc
to babel.config.js
worked for me.
Sample babel.config.js
-
module.exports = {
presets: ["@babel/preset-env", "@babel/preset-react"],
plugins: ["@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import"],
};
The issue here was with the way Babel was compiling Preact. I had to add the @babel/plugin-transform-react-jsx plugin in order to get my Jest testing working.
Turns out it is vaguely documented in the Preact docs in the Global pragma
section.
npm i @babel/plugin-transform-react-jsx --save-dev
.babelrc
{
"presets": [
[
"@babel/preset-env", {
"targets": {
"node": "current"
}
},
"@babel/preset-react"
]
],
"plugins": [
[
"@babel/plugin-transform-runtime", {
"regenerator": true
}
],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-export-default-from",
["@babel/plugin-transform-react-jsx", { "pragma":"h" }]
]
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With