尝试设置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} />,
| ^
我已经尝试过文档和类似问题,但是没有运气! 似乎Jest并没有使用我的babel设置。
任何想法如何消除错误?
{
"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
文件中:
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>
);
}
}
答案 0 :(得分:0)
我认为您写错了。
应该是:
render() {
return (
<Gallery ...
)
}
答案 1 :(得分:0)
有同样的问题。将.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"],
};
答案 2 :(得分:0)
这里的问题是Babel编译Preact的方式。我必须添加@babel/plugin-transform-react-jsx插件才能使Jest测试正常工作。
结果证明,它在import React from 'react';
import { StyleSheet, Text, View, ActivityIndicator } from 'react-native';
import AppNavigator from './src/AppNavigator';
//Warning
import {Font} from 'expo';
//import * as Font from 'expo-font';
export default class App extends React.Component {
//Warning
constructor(){
super()
this.state={
IsLoaded:false
}
}
//Warning
async componentWillMount(){
await Font.loadAsync() ({
'Roboto_medium': require('./assets/fonts/Roboto_medium.ttf'),
});
this.setState({IsLoaded: true});
}
render()
{
return (
<View>
{this.state.IsLoaded ? (
//this.state.loading
<Text>Open up App.js to start working on your app!</Text>
//<AppNavigator/>
) : (
<Text>You Failed!</Text>
//<AppNavigator/>
)}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
部分的Preact docs中模糊地记录着。
Global pragma
npm i @babel/plugin-transform-react-jsx --save-dev
.babelrc