开玩笑-不变式失败:您不应在<路由器>

时间:2020-10-01 00:21:48

标签: jestjs react-router-dom

运行测试时遇到此错误,但是我将要测试的组件包装在<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');
            });
    });
});

login-page.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 个答案:

答案 0 :(得分:0)

正如消息所言,您不能使用没有任何Link类型父项的Router。在您的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>`);