对React组件进行开玩笑的测试:意外的令牌“<" [英] Jest tests on React components: Unexpected token "<"

查看:71
本文介绍了对React组件进行开玩笑的测试:意外的令牌“<"的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

尝试设置Jest来测试我的React组件(从技术上讲,我使用的是Preact),但想法相同...

Trying to set up Jest to test my React components (Technically I'm using Preact) but same idea...

每次尝试获取覆盖率报告时,如果遇到任何jsx语法,我都会出错.

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} />,
     |   ^

我已经尝试过文档和类似问题,但是没有运气! 看来Jest并没有使用我的babel设置.

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"
  ]
}

我的组件像这样被加载到我的index.js文件中:

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>
    );
  }
}

推荐答案

这里的问题是Babel编译Preact的方式.我必须添加 @ babel/plugin-transform-react-jsx 插件以使我的Jest测试正常工作.

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.

事实证明,它被模糊地记录在精确文档Global pragma部分.

Turns out it is vaguely documented in the Preact docs in the Global pragma section.

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

2.更新.babelrc

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" }]
  ]
}

这篇关于对React组件进行开玩笑的测试:意外的令牌“&lt;"的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆