开玩笑 - 不变式失败:您不应该在 <Router> 之外使用 <Link>
Posted
技术标签:
【中文标题】开玩笑 - 不变式失败:您不应该在 <Router> 之外使用 <Link>【英文标题】:Jest - Invariant failed: You should not use <Link> outside a <Router> 【发布时间】:2021-01-16 17:04:03 【问题描述】:我在运行测试时收到此错误,但是我已将我正在测试的组件包装在 <BrowserRouter>
组件中:
● Axios › 得到响应
Invariant failed: You should not use <Link> outside a <Router> 91 | 92 | if (RootComponent) > 93 | const component = mountWithCustomWrappers(<Wrapper store=store><RootComponent
...props />, rootWrappers);
nock.integration.test
import React from 'react';
import axios from 'axios';
const core = require('tidee-life-core');
import httpAdapter from 'axios/lib/adapters/http';
import doFetch from '../../../helpers/doFetch.js';
import ComponentBuilder from "../component-builder";
import LoginPage from "../../../scenes/login/login-page";
const host = 'http://example.com';
process.env.API_URL = host;
axios.defaults.host = host;
axios.defaults.adapter = httpAdapter;
const makeRequest = () =>
return doFetch(
url: core.urls.auth.login(),
queryParams: foo: 'bar' ,
)
.then(res => res.data)
.catch(error => console.log(error));
;
describe('Axios', () =>
let component;
let componentBuilder;
beforeEach(() =>
componentBuilder = new ComponentBuilder();
);
test('gets a response', async () =>
componentBuilder.includeInterceptor('login');
component = await componentBuilder.build(
RootComponent: LoginPage,
selector: 'LoginForm',
);
return makeRequest()
.then(response =>
expect(typeof response).toEqual('object');
expect(response.data.token).toEqual('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhNDY3MGE3YWI3ZWM0ZjQ2MzM4ODdkMzJkNzRkNTY5OSIsImlhdCI6MTU1MjA5MDI1NX0.vsKLXJEqSUZK-Y6IU9PumfZdW7t1SLM28jzJL89lcrA');
);
);
);
登录页面.jsx:
import React, Component from 'react';
import PropTypes from "prop-types";
import withRouter from 'react-router-dom';
import injectIntl, intlShape from 'react-intl';
import queryString from 'query-string';
import Link from 'tidee-life-ui';
import LoginForm from './login-form.jsx';
import doLogin from '../../api/auth/auth-api';
import Auth from '../../modules/Auth';
import messages from '../../messages';
const urls = require('tidee-life-core');
class LoginPage extends Component
constructor(props)
super(props);
const intl = props;
if (Auth.isUserAuthenticated())
props.history.replace( pathname: urls.pages.pathBoxes() );
this.messages =
'account.activation.error.expired': intl.formatMessage(messages['account.activation.error.expired']),
'account.activation.required': intl.formatMessage(messages['account.activation.required']),
'common.click': intl.formatMessage(messages['common.click']),
'common.here': intl.formatMessage(messages['common.here']),
'error.500.msg': intl.formatMessage(messages['error.500.msg']),
'forgot.success': intl.formatMessage(messages['forgot.success']),
'login.account.needs.activating.partial': intl.formatMessage(messages['login.account.needs.activating.partial']),
'login.error.account.credentials': intl.formatMessage(messages['login.error.account.credentials']),
'login.validation.email': intl.formatMessage(messages['login.validation.email']),
'login.validation.password': intl.formatMessage(messages['login.validation.password']),
'signup.account.created': intl.formatMessage(messages['signup.account.created'])
;
let alertMessage;
let alertMessageType;
const query = queryString.parse(props.location.search);
if ('signup-success' in query)
alertMessage = this.messages['signup.account.created'];
alertMessageType = 'success';
else if ('forgot-success' in query)
alertMessage = this.messages['forgot.success'];
alertMessageType = 'success';
this.state =
alert:
type: alertMessageType ? alertMessageType : '',
msg: alertMessage ? alertMessage : '',
,
user:
email: '',
password: ''
;
this.changeUser = this.changeUser.bind(this);
this.clearAlert = this.clearAlert.bind(this);
this.processForm = this.processForm.bind(this);
clearAlert()
this.setState( alert:
type: '',
msg: '',
);
processForm(e)
e.preventDefault();
return doLogin(
email: this.state.user.email,
password: this.state.user.password,
).then((response) =>
Auth.authenticateUser(response.data.token);
this.props.history.replace( pathname: urls.pages.pathBoxes() );
).catch((error) =>
const msg = error.message && this.messages[error.message] ? [this.messages[error.message]] : [this.messages['error.500.msg']];
if (error.message === 'account.activation.error.expired' || error.message === 'account.activation.required')
const to = urls.pages.pathResendLink(error.data.confirmHash);
msg.push(` $this.messages['common.click'] `);
msg.push(<Link underline color="inherit" key="email" to=to>this.messages['common.here']</Link>);
msg.push(` $this.messages['login.account.needs.activating.partial']`);
this.setState(
alert:
type: 'error',
msg,
);
);
changeUser(event)
const name, value = event.target;
this.setState((currentState) => (
user:
...currentState.user,
[name]: value,
));
render()
return (
<LoginForm
data-test="login-form"
alert=this.state.alert
onSubmit=this.processForm
onChange=this.changeUser
user=this.state.user
/>
);
LoginPage.propTypes =
history: PropTypes.object,
intl: intlShape.isRequired,
location: PropTypes.object.isRequired,
;
export default injectIntl(withRouter(LoginPage));
component-builder.js
import React from "react";
import nock from 'nock';
import cloneDeep from 'lodash.clonedeep';
import mountWithCustomWrappers from 'enzyme-custom-wrappers';
import waitForStoreState from './wait/wait-for-store-state';
import Wrapper from './wrapper.jsx';
import waitForComponentPredicate from './wait-for-component-predicate/wait-for-component-predicate';
import waitForComponentSelector from './wait-for-component-selector/wait-for-component-selector';
import startAllNockServiceIntercepts from './nock/nock-manager';
import nockServices from './nock/services';
import store from "../../store/store";
import wrappers from './wrappers';
const rootWrappers = component => wrappers(component);
class ComponentBuilder
constructor()
this.nockInterceptors = [];
this.storePreparers = [];
includeInterceptor( interceptorName, nockConfigOverride = null )
// Maybe need to do a clone deep here if things start breaking!
const clonedNockService = cloneDeep(nockServices[interceptorName]().nockConfig);
const nockService =
[interceptorName]:
...clonedNockService,
...(nockConfigOverride || ),
;
this.nockInterceptors.push(nockService);
prepareStore( storePreparer )
this.storePreparers.push(storePreparer);
async waitForStoreToUpdate()
const promises = this.storePreparers
.map(service => waitForStoreState(service.redux.storeStateToWaitFor, store));
await Promise.all(promises);
async runStorePreparers()
nock.cleanAll();
const interceptors = [];
this.storePreparers.forEach((service) =>
const interceptorName = service.http.interceptor;
const clonedNockService = service.http.interceptor && cloneDeep(nockServices[interceptorName]().nockConfig);
interceptors.push(
[interceptorName]:
...clonedNockService,
);
);
startAllNockServiceIntercepts(interceptors);
this.storePreparers.forEach(service => service.redux.actionToDispatch && store.dispatch(service.redux.actionToDispatch()));
return await this.waitForStoreToUpdate();
/**
* Build a component to be tested.
* @param RootComponent
* @param selector string - A selector to wait for. CSS selector or name of component.
* @param props object
* @param store object
* @param predicate function - A function that returns true if a condition is met.
* @param predicateMaxTime number
* @param predicateInterval number
* @returns Promise<*>
*/
async build(
RootComponent = null,
selector = '',
props = ,
predicate = null,
predicateMaxTime = 2000,
predicateInterval = 10,
= )
try
await this.runStorePreparers();
startAllNockServiceIntercepts(this.nockInterceptors);
if (RootComponent)
const component = mountWithCustomWrappers(<Wrapper store=store><RootComponent ...props /></Wrapper>, rootWrappers);
if (selector)
await waitForComponentSelector( selector, rootComponent: component, store );
if (predicate)
await waitForComponentPredicate(
predicate,
rootComponent: component,
store,
maxTime: predicateMaxTime,
interval: predicateInterval,
);
return component;
catch(err)
throw err;
export default ComponentBuilder;
wrapper.jsx
import PropTypes from 'prop-types';
import React, Component from 'react';
import BrowserRouter from "react-router-dom";
import Provider from 'react-redux';
import ThemeProvider from "../../theme/Theme.jsx";
import LocaleProviderWrapper from "./locale-provider-wrapper.jsx";
const propTypes =
children: PropTypes.element.isRequired,
store: PropTypes.object.isRequired,
;
class Wrapper extends Component
getStore()
return this.props.store;
render()
return (
<Provider store=this.props.store>
<LocaleProviderWrapper>
<ThemeProvider>
<BrowserRouter>this.props.children</BrowserRouter>
</ThemeProvider>
</LocaleProviderWrapper>
</Provider>
);
Wrapper.propTypes = propTypes;
export default Wrapper;
【问题讨论】:
【参考方案1】:正如消息所说,您不能使用没有Router
类型的任何父级的Link
。在 processForm
函数中,您正在使用已磨损的 Link
组件构建消息。
if (error.message === 'account.activation.error.expired' || error.message === 'account.activation.required')
const to = urls.pages.pathResendLink(error.data.confirmHash);
msg.push(` $this.messages['common.click'] `);
msg.push(<Link underline color="inherit" key="email" to=to>this.messages['common.here']</Link>);
msg.push(` $this.messages['login.account.needs.activating.partial']`);
你应该使用a
标签来建立动态链接。可能是这样的:
msg.push(`<a href="mailto:$this.to">$this.messages['common.here']</a>`);
【讨论】:
即使包含 组件的这段代码没有运行,是否会触发错误? 我相信是的,你可以把链接去掉,看看是否还有错误。 代码仍然需要从 JSX 编译成 javascript。 @JoeTidee 这是否解决了您的问题?以上是关于开玩笑 - 不变式失败:您不应该在 <Router> 之外使用 <Link>的主要内容,如果未能解决你的问题,请参考以下文章
错误:不变量失败:您不应该在 <Router> 之外使用 <Link> 进行反应
React 测试库不变量失败:您不应该在 <Router> 之外使用 <Route>
不变违规:您不应该在 <Router> 之外使用 <Switch>
不变违规:您不应该在 <Router> 之外使用 withRouter() (使用最少的工作示例)