Jest tests on React components: Unexpected token "<"
Asked Answered
D

3

12

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.

Error

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?

package.json

{
  "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/**"
    ]
  }
}

.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-export-extensions"
  ]
}

Edit

My component is loaded into my index.js file like so:

index.js

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'),
);

Gallery.js

/** @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>
    );
  }
}
Dermal answered 9/7, 2019 at 12:35 Comment(0)
D
5

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.

Solution

1. Install plugin

npm i @babel/plugin-transform-react-jsx --save-dev

2. Update .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" }]
  ]
}
Dermal answered 10/7, 2019 at 7:59 Comment(0)
A
5

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"],
};
Accusal answered 9/7, 2019 at 13:4 Comment(5)
Yeah I saw that in a few of the other answers but doing that still gives me the error when running a test and it throws Support for the experimental syntax 'classProperties' in not enabled when compiling the actual component ¯\_(ツ)_/¯Dermal
Oh, and at what code does this error comes at? @EliNathanAccusal
Also have you used create-react-app or did your own webpack config? And please share the code of the component that you are testing.Accusal
No webpack at all! I'm using parcel to compile everything as I recreated my project quickly to see if I could get Jest working. I was using Gulp in my main project but that gave me the exact same errors. See my edits for the component codeDermal
And when I switch it over to babel.config.js I get the error around 'classProperties' at my handleKeyDown = (event) => {} functionDermal
D
5

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.

Solution

1. Install plugin

npm i @babel/plugin-transform-react-jsx --save-dev

2. Update .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" }]
  ]
}
Dermal answered 10/7, 2019 at 7:59 Comment(0)
O
-1

I think that you are writing it out incorrectly.

It should be:

render() {
  return (
    <Gallery ...
  )
}
Oppugn answered 9/7, 2019 at 12:44 Comment(2)
That breaks the actual component and then the error points to the the { character insteadDermal
See the Preact docs for why that's not needed in this instance :)Dermal

© 2022 - 2024 — McMap. All rights reserved.