我正在尝试使用Enzyme测试React组件。测试运行良好,直到我们将组件转换为钩子。现在我得到了错误,"错误:未捕获[TypeError:无法读取未定义的属性'history']">
我已经通读了以下类似的问题,但无法解决:
- 反应Jest/酶测试:使用历史挂钩断裂测试
- 酶法检测反应组分
- Jest&Hooks:TypeError:无法读取属性';Symbol(Symbol.iterator(';个,共个未定义
还有这篇文章:*https://medium.com/7shifts-engineering-blog/testing-usecontext-react-hook-with-enzyme-shallow-da062140fc83
完整组件,AccessBarWithRouter.jsx:
/**
* @description Accessibility bar component to allow user to jump focus to different components on screen.
* One dropdown will focus to elements on screen.
* The other will route you to routes in your navigation bar.
*
*/
import React, { useState, useEffect, useRef } from 'react';
import Dropdown from 'react-dropdown-aria';
import { useHistory } from 'react-router-dom';
const AccessBarWithRouter = () => {
const pathname = useHistory().location.pathname;
const [sectionInfo, setSectionInfo] = useState(null);
const [navInfo, setNavInfo] = useState(null);
const [isHidden, setIsHidden] = useState(true);
// creating the refs to change focus
const sectionRef = useRef(null);
const accessBarRef = useRef(null);
// sets focus on the current page from the 1st dropdown
const setFocus = e => {
const currentLabel = sectionInfo[e];
const currentElement = document.querySelector(`[aria-labelledBy='${currentLabel}']`);
currentElement.tabIndex = -1;
sectionRef.current = currentElement;
// can put a .click() after focus to focus with the enter button
// works, but gives error
sectionRef.current.focus();
};
// Changes the page when selecting a link from the 2nd dropdown
const changeView = e => {
const currentPath = navInfo[e];
const accessLinks = document.querySelectorAll('.accessNavLink');
accessLinks.forEach(el => {
if (el.pathname === currentPath) {
el.click();
};
});
};
// event handler to toggle visibility of AccessBar and set focus to it
const accessBarHandlerKeyDown = e => {
if (e.altKey && e.keyCode === 191) {
if (isHidden) {
setIsHidden(false)
accessBarRef.current.focus();
} else setIsHidden(true);
}
}
/**
*
* useEffect hook to add and remove the event handler when 'alt' + '/' are pressed
* prior to this, multiple event handlers were being added on each button press
* */
useEffect(() => {
document.addEventListener('keydown', accessBarHandlerKeyDown);
const navNodes = document.querySelectorAll('.accessNavLink');
const navValues = {};
navNodes.forEach(el => {
navValues[el.text] = el.pathname;
});
setNavInfo(navValues);
return () => document.removeEventListener('keydown', accessBarHandlerKeyDown);
}, [isHidden]);
/**
* @todo figure out how to change the dropdown current value after click
*/
useEffect(() => {
// selects all nodes with the aria attribute aria-labelledby
setTimeout(() => {
const ariaNodes = document.querySelectorAll('[aria-labelledby]');
let sectionValues = {};
ariaNodes.forEach(node => {
sectionValues[node.getAttribute('aria-labelledby')] = node.getAttribute('aria-labelledby');
});
setSectionInfo(sectionValues);
}, 500);
}, [pathname]);
// render hidden h1 based on isHidden
if (isHidden) return <h1 id='hiddenH1' style={hiddenH1Styles}>To enter navigation assistant, press alt + /.</h1>;
// function to create dropDownKeys and navKeys
const createDropDownValues = dropDownObj => {
const dropdownKeys = Object.keys(dropDownObj);
const options = [];
for (let i = 0; i < dropdownKeys.length; i++) {
options.push({ value: dropdownKeys[i]});
}
return options;
};
const sectionDropDown = createDropDownValues(sectionInfo);
const navInfoDropDown = createDropDownValues(navInfo);
return (
<div className ='ally-nav-area' style={ barStyle }>
<div className = 'dropdown' style={ dropDownStyle }>
<label htmlFor='component-dropdown' tabIndex='-1' ref={accessBarRef} > Jump to section: </label>
<div id='component-dropdown' >
<Dropdown
options={ sectionDropDown }
style={ activeComponentDDStyle }
placeholder='Sections of this page'
ariaLabel='Navigation Assistant'
setSelected={setFocus}
/>
</div>
</div>
<div className = 'dropdown' style={ dropDownStyle }>
<label htmlFor='page-dropdown'> Jump to page: </label>
<div id='page-dropdown' >
<Dropdown
options={ navInfoDropDown }
style={ activeComponentDDStyle }
placeholder='Other pages on this site'
ariaLabel='Navigation Assistant'
setSelected={ changeView }
/>
</div>
</div>
</div>
);
};
/** Style for entire AccessBar */
const barStyle = {
display: 'flex',
paddingTop: '.1em',
paddingBottom: '.1em',
paddingLeft: '5em',
alignItems: 'center',
justifyContent: 'flex-start',
zIndex: '100',
position: 'sticky',
fontSize: '.8em',
backgroundColor: 'gray',
fontFamily: 'Roboto',
color: 'white'
};
const dropDownStyle = {
display: 'flex',
alignItems: 'center',
marginLeft: '1em',
};
/** Style for Dropdown component **/
const activeComponentDDStyle = {
DropdownButton: base => ({
...base,
margin: '5px',
border: '1px solid',
fontSize: '.5em',
}),
OptionContainer: base => ({
...base,
margin: '5px',
fontSize: '.5em',
}),
};
/** Style for hiddenH1 */
const hiddenH1Styles = {
display: 'block',
overflow: 'hidden',
textIndent: '100%',
whiteSpace: 'nowrap',
fontSize: '0.01px',
};
export default AccessBarWithRouter;
这是我的测试,AccessBarWithRouter.unit.test.js:
import React from 'react';
import Enzyme, { mount } from 'enzyme';
import AccessBarWithRouter from '../src/AccessBarWithRouter.jsx';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
describe('AccessBarWithRouter component', () => {
it('renders hidden h1 upon initial page load (this.state.isHidden = true)', () => {
const location = { pathname: '/' };
const wrapper = mount(
<AccessBarWithRouter location={location}/>
);
// if AccessBarWithRouter is hidden it should only render our invisible h1
expect(wrapper.exists('#hiddenH1')).toEqual(true);
})
it('renders full AccessBarWithRouter when this.state.isHidden is false', () => {
// set dummy location within test to avoid location.pathname is undefined error
const location = { pathname: '/' };
const wrapper = mount(
<AccessBarWithRouter location={location} />
);
wrapper.setState({ isHidden: false }, () => {
// If AccessBar is not hidden the outermost div of the visible bar should be there
// Test within setState waits for state change before running test
expect(wrapper.exists('.ally-nav-area')).toEqual(true);
});
});
});
我是React Hooks的新手,所以我试着把我的注意力集中在它上。我的理解是,我必须为我的测试提供某种模拟历史值。我试着创建一个单独的useContext文件,并在测试中将其包装在我的组件上,但没有成功:
import React, { useContext } from 'react';
export const useAccessBarWithRouterContext = () => useContext(AccessBarWithRouterContext);
const defaultValues = { history: '/' };
const AccessBarWithRouterContext = React.createContext(defaultValues);
export default useAccessBarWithRouterContext;
我的devDependencies:的当前版本
- "@babel/cli":"^7.8.4">
- "@babel/core":"^7.8.6">
- "@babel/polyfill":"^7.0.0-beta.51">
- "@babel/preset-env":"^7.8.6">
- "@babel/preset-react":"^7.8.3">
- "babel core":"^7.0.0-bridge.0">
- "babel笑话":"^25.1.0">
- "酶":"^3.3.0">
- "酶适配器反应器-16":"^1.1.1">
- "笑话":"^25.1.0">
- "反应":"^16.13.0">
- "react dom":"^16.13.0">
我没有找到太多的文档来测试通常使用useHistory挂钩的组件。Enzyme似乎一年前才开始使用React Hooks,而且只用于模拟,而不用于浅层渲染。
有人知道我该怎么做吗?
您可以想象,这里的问题来自useHistory钩子内部。挂钩设计用于路由器提供商的消费者。如果你知道提供者和消费者的结构,那么消费者(useHistory(试图访问提供者的一些信息对你来说是完全合理的,而这些信息在你的文本框中并不存在。有两种可能的解决方案:
-
用路由器包装您的测试用例
it('renders hidden h1 upon initial page load (this.state.isHidden = true)', () => { const location = { pathname: '/' }; const wrapper = mount( <Router> <AccessBarWithRouter location={location}/> </Router> ) });
-
模拟useHistory挂钩与一个假的历史数据
jest.mock('react-router-dom', () => { const actual = require.requireActual('react-router-dom') return { ...actual, useHistory: () => ({ methods }), } })
我个人更喜欢第二个,因为你可以把它放在setupTests文件中,然后忘记它。如果您需要模拟或监视它,您可以在特定的单元测试文件中覆盖setupTests文件中的mock。