对 React 组件的玩笑测试:意外的标记“<”

Posted

技术标签:

【中文标题】对 React 组件的玩笑测试:意外的标记“<”【英文标题】:Jest tests on React components: Unexpected token "<" 【发布时间】:2019-11-19 00:28:41 【问题描述】:

尝试设置 Jest 来测试我的 React 组件(技术上我正在使用 Preact)但同样的想法......

每当我尝试获取覆盖率报告时,都会在遇到任何 jsx 语法时出错。

错误

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

我已尝试关注文档和类似问题,但没有运气! 好像我的 babel 设置没有被 Jest 使用。

知道如何摆脱错误吗?

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

编辑

我的组件被加载到我的index.js 文件中,如下所示:

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

图库.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>
    );
  

【问题讨论】:

【参考方案1】:

这里的问题在于 Babel 编译 Preact 的方式。我必须添加 @babel/plugin-transform-react-jsx 插件才能让我的 Jest 测试正常工作。

原来它在Global pragma 部分的Preact docs 中有模糊的记录。

解决方案

1。安装插件

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

2。更新.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" ]
  ]

【讨论】:

【参考方案2】:

遇到了同样的问题。将 .babelrc 重命名为 babel.config.js 对我有用。

样本babel.config.js -

module.exports = 
  presets: ["@babel/preset-env", "@babel/preset-react"],
  plugins: ["@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import"],
;

【讨论】:

是的,我在其他一些答案中看到了这一点,但是这样做仍然会在运行测试时给我错误,并且在编译实际组件时会抛出 Support for the experimental syntax 'classProperties' in not enabled¯\_(ツ)_/ ¯ 哦,这个错误出现在什么代码上? @EliNathan 你也使用过 create-react-app 或者你自己的 webpack 配置吗?请分享您正在测试的组件的代码。 根本没有 webpack!我正在使用 parcel 编译所有内容,因为我快速重新创建了我的项目,看看我是否可以让 Jest 工作。我在我的主要项目中使用 Gulp,但这给了我完全相同的错误。查看我对组件代码的编辑 当我将其切换到 babel.config.js 时,我在 handleKeyDown = (event) =&gt; 函数中收到有关“classProperties”的错误【参考方案3】:

我认为你写错了。

应该是:

render() 
  return (
    <Gallery ...
  )

【讨论】:

这会破坏实际组件,然后错误指向 字符 查看Preact docs,了解为什么在这种情况下不需要这样做:)

以上是关于对 React 组件的玩笑测试:意外的标记“<”的主要内容,如果未能解决你的问题,请参考以下文章

用 Create React App 开玩笑:测试套件无法运行 - 意外的令牌

导入图像打破了开玩笑的测试

如何在 react/redux 应用程序中开玩笑地访问组件的子组件

开玩笑,意外的令牌导入

组件中的 ReactJS 意外标记

开玩笑设置“SyntaxError:意外的令牌导出”