对 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) =>
函数中收到有关“classProperties”的错误【参考方案3】:
我认为你写错了。
应该是:
render()
return (
<Gallery ...
)
【讨论】:
这会破坏实际组件,然后错误指向
字符
查看Preact docs,了解为什么在这种情况下不需要这样做:)以上是关于对 React 组件的玩笑测试:意外的标记“<”的主要内容,如果未能解决你的问题,请参考以下文章
用 Create React App 开玩笑:测试套件无法运行 - 意外的令牌