import React, { Component } from 'react';
import { shallow } from 'enzyme';
class App extends Component {
a = {
func: () => 1
};
render () {
return null;
}
}
describe('<App>', () => {
test('func()', () => {
const app = shallow(<App />),
func_spy = jest.spyOn(???);
});
});
I want to spy on func function at a class property. Can I achieve it using spyOn method or something? Thank you in advance for your reply.
You can get the class instance using .instance and use it to create the spy:
import React, { Component } from 'react';
import { shallow } from 'enzyme';
class App extends Component {
a = {
func: () => 1
};
render() {
return null;
}
}
describe('<App>', () => {
test('func()', () => {
const app = shallow(<App />);
const instance = app.instance(); // <= get the instance
const func_spy = jest.spyOn(instance.a, 'func'); // <= spy on instance.a.func
instance.a.func();
expect(func_spy).toHaveBeenCalled(); // Success!
});
});
Related
Hello I have custom hook
code is like below
import * as React from 'react'
export const myCustomHook = (props?: boolean) => {
const [value, setValue] = React.useState([]);
React.useEffect(() => {
return (async (p1) => {
// ....
setValue(someValues)
})
}, [])
const myFun = async (prop1) => {
// ... some operations
return {id: id}
}
return { myFun, value }
}
I am using the above like this
const { value, myFun } = myCustomHook();
const foofun = async (pp) => {
const myfunRes = await myFun(prop1);
}
Now I want to put myFun in useEffect
Please help me with this.
I'm getting this error when running a test, however I have wrapped the component I am testing in a <BrowserRouter> component:
● Axios › gets a response
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;
Exactly as message says, you cannot use Link which doesn't have any parent of type Router. In you processForm function you are building a messaage with Link component which is worng.
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']}`);
}
You should use a tag to build dynamic link. May be something like:
msg.push(`${this.messages['common.here']}`);
I am using NextJS with next-i18next. This is my home page:
import {withTranslation} from '../config/next-i18next';
const Home = function Home() {
return (<div>test</div>)
};
Home.getInitialProps = async () => {
return {namespacesRequired: ['home']}
};
export default withTranslation('home')(Home);
What I want is to get current language inside a component/page, how can I do that ?
withTranslation injects the i18n object.
import {withTranslation} from '../config/next-i18next';
const Home = function Home({ i18n }) {
return (<div>{i18n.language}</div>)
// ----------------^
};
Home.getInitialProps = async () => {
return {namespacesRequired: ['home']}
};
export default withTranslation('home')(Home);
Or using Hooks,
import {useTranslation} from '../config/next-i18next';
const Home = function Home() {
const { i18n } = useTranslation('home');
return (<div>{i18n.language}</div>)
// ----------------^
};
Home.getInitialProps = async () => {
return {namespacesRequired: ['home']}
};
export default Home;
With Next.js you could also use the useRouter hook.
import {withTranslation} from '../config/next-i18next';
import { useRouter } from 'next/router'
const Home = function Home() {
const router = useRouter()
const currentLang = router.locale // => locale string eg. "en"
return (<div>test</div>)
};
Home.getInitialProps = async () => {
return {namespacesRequired: ['home']}
};
export default withTranslation('home')(Home);
I'm trying to implement Bullet train API in a React web app. According to their node client documentation, I have setup the following function:
export const isFeatureEnabled = async (nameOfTheFeature) => {
return new Promise((resolve) => {
bulletTrain.init({
environmentID: BULLET_TRAIN_ENV_ID
});
bulletTrain.hasFeature(nameOfTheFeature)
.then((featureFlag) => {
if (featureFlag[nameOfTheFeature].enabled) {
resolve(true);
}
})
.catch(err => resolve(false));
});
}
This is called in regular components like this:
render() {
return (<div>{await isFeatureEnabled('feature1') && <p>feature1 is enabled</p>}</div>)
};
which throws this:
Parsing error: Can not use keyword 'await' outside an async function
If we add the async keyword, with a proper return statement:
async render() {
return (<div>{await isFeatureEnabled('feature1') && <p>feature1 is enabled</p>}</div>)
};
Then it throws:
Your render method should have return statement
So what is the correct way to use this promised function inside a react app?
I would suggest you not to use await keyword in render instead use componentDidMount and constructor for this and use state object to check:
constructor(props){
super(props);
this.state = { isFeatEnabled: false };
}
componentDidMount(){
this.setState({isFeatEnabled:isFeatureEnabled('feature1')})
}
Now in the render:
render() {
return (<div>{this.state.isFeatEnabled && <p>feature1 is enabled</p>}</div>)
};
And remove the async from the method.
call function isFeatureEnabled inside an async function during mount (before/after your wish)
example -
export const isFeatureEnabled = async (nameOfTheFeature) => {
return new Promise((resolve) => {
bulletTrain.init({
environmentID: BULLET_TRAIN_ENV_ID
});
bulletTrain.hasFeature(nameOfTheFeature)
.then((featureFlag) => {
if (featureFlag[nameOfTheFeature].enabled) {
resolve(true);
}
})
.catch(err => resolve(false));
});
}
...
componentDidMount() {
this.checEnabled();
}
...
const checkEnabled = async () => {
const flag = await isFeatureEnabled('feature1');
this.setState({f1enabled: flag});
}
...
render() {
return (<div>{this.state.f1enabled ? <p>feature1 is enabled</p> : null}</div>)
}
If isFeatureEnabled is in the same file keep it outside class component or else keep it in another file and export the function.
You can't use promise at there, the proper way:
import React, { useEffect, useState } from 'react'
import bulletTrain from '../somewhere'
import BULLET_TRAIN_ENV_ID from '../somewhere'
export default function featureComponent({ featureName }) {
const [featureEnabled, setFeatureEnabled] = useState(false)
useEffect(() => {
bulletTrain.init({
environmentID: BULLET_TRAIN_ENV_ID
})
bulletTrain
.hasFeature(featureName)
.then(featureFlag => {
if (featureFlag[featureName].enabled) {
setFeatureEnabled(true)
}
})
.catch(err => setFeatureEnabled(false))
}, [featureName])
return <div>{featureEnabled && <p>{featureName} is enabled</p>}</div>
}
Append isFeatureEnabled function re-use answer below:
import React, { useEffect, useState } from 'react'
import isFeatureEnabled from '../somewhere'
export default function featureComponent({ featureName }) {
const [featureEnabled, setFeatureEnabled] = useState(false)
useEffect(() => {
const checkAndSetEnabled = async () => {
const enabled = await isFeatureEnabled(featureName)
setFeatureEnabled(enabled)
}
checkAndSetEnabled()
}, [featureName])
return <div>{featureEnabled && <p>{featureName} is enabled</p>}</div>
}
I have the following method skipLoggingThisRequest in a node js class which I am trying to test. The method is supposed to return either true or false, based on the path in request, using ramda compose to get to that value. However in my tests, no matter what path I set in the request object, my skipLoggingThisRequest always returns true.
What am I missing here?
my class:
import { compose, filter, join, toPairs, map, prop, flip, contains, test, append } from 'ramda'
import { create, env } from 'sanctuary'
import { isEmpty, flattenDeep } from 'lodash'
import chalk from 'chalk'
import log from 'menna'
class MyClass {
constructor (headerList) {
this.headerWhiteList = flattenDeep(append(headerList, []));
}
static getBody (req) {
return (!isEmpty(req.body) ? JSON.stringify(req.body) : '');
}
static S () {
return create({ checkTypes: false, env });
}
static isInList () {
return flip(contains);
}
static isInWhitelist () {
return compose(this.isInList(this.headerWhiteList), this.S.maybeToNullable, this.S.head);
}
static parseHeaders () {
return (req) => compose(join(','), map(join(':')), filter(this.isInWhitelist), toPairs, prop('headers'));
}
skipLoggingThisRequest () {
return (req) => compose(test(/^.*(swagger|docs|health).*$/), prop('path'))
}
logger (req, res, next) {
if (this.skipLoggingThisRequest(req)) {
console.log('Skipping')
return next();
}
const primaryText = chalk.inverse(`${req.ip} ${req.method} ${req.originalUrl}`);
const secondaryText = chalk.gray(`${this.parseHeaders(req)} ${this.getBody(req)}`);
log.info(`${primaryText} ${secondaryText}`);
return next();
}
}
export default MyClass
My tests:
import sinon from 'sinon';
import MyClass from '../lib/MyClass';
describe('MyClass', () => {
const headerList = ['request-header-1', 'request-header-2'];
const request = {
'headers': {
'request-header-1': 'yabadaba',
'request-header-2': 'dooooooo'
},
'ip': 'shalalam',
'method': 'GET',
'originalUrl': 'http://myOriginalUrl.com',
'body': ''
};
const response = {};
const nextStub = sinon.stub();
describe('Logs request', () => {
const myInstance = new MyClass(headerList);
const skipLogSpy = sinon.spy(myInstance, 'skipLoggingThisRequest');
request.path = '/my/special/path';
myInstance.logger(request, response, nextStub);
sinon.assert.called(nextStub);
});
});
this.skipLoggingThisRequest(req) returns a function ((req) => compose(test(/^.*(swagger|docs|health).*$/), prop('path'))).
It does not return a boolean. However, since functions are truthy, your if statement always executes.
What you most likely want to do is this.skipLoggingThisRequest()(req). You get the function and then apply a request to it.
Demonstration of what's going on:
const testFunction = () => (test) => test === "Hello!";
console.log(testFunction);
console.log(testFunction());
console.log(testFunction()("Hello!"));
console.log(testFunction()("Goodbye!"));
if (testFunction) {
console.log("testFunction is truthy.");
}
if (testFunction()) {
console.log("testFunction() is truthy.");
}
if (testFunction()("Hello!")) {
console.log('testFunction()("Hello!") is truthy.');
}
if (!testFunction()("Goodbye!")) {
console.log('testFunction()("Goodbye!") is falsey.');
}