Is it possible to live reload react-intl messages during development(for default language)?
I mean like Hot Module Loading, only updated message should be affected. Any ohter solution without running extra script or refreshing whole page will work too.
Thank you.
In case anybody need it, I wrote HOC for this;
import React, {Component} from "react";
import {IntlProvider} from "react-intl";
const url = location.protocol + '//' + location.host + "/";
class IntlLoader extends Component {
constructor(props) {
super(props);
const {initialLocale: locale, initialMessages: messages} = props;
this.state = {locale: 'en', messages};
}
fetchLanguagesForDevelopment = () => {
// if development, use hot loading
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
this.setState({...this.state, loading: true})
fetch(url + "reactIntlMessages.json")
.then((res) => {
return res.json();
})
.then((messages) => {
this.setState({loading: false})
if (messages !== this.state.messages)
this.setState({...this.state, messages})
})
.catch((error) => {
this.setState({error, loading: false})
})
} else {
const messages = require('../../dist/reactIntlMessages.json')
if (this.state.messages !== messages)
this.setState({...this.state, messages, loading: false})
}
}
componentDidMount() {
this.fetchLanguagesForDevelopment()
}
componentWillReceiveProps(nextProps) {
this.fetchLanguagesForDevelopment()
}
render() {
const {error, messages, loading} = this.state;
//if (loading) return (<div>Please wait...</div>)
if (error) return (<div>Error While Loading Language</div>)
return (
<IntlProvider {...this.state}>
{this.props.children}
</IntlProvider>
);
}
}
export default IntlLoader
You can module.hot.accept your translated messages and render it as argument. See this example in react-boilerplate
https://github.com/react-boilerplate/react-boilerplate/blob/v3.5.0/app/app.js
const render = (messages) => {
ReactDOM.render(
<Provider store={store}>
<LanguageProvider messages={messages}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</LanguageProvider>
</Provider>,
MOUNT_NODE
);
};
if (module.hot) {
// Hot reloadable React components and translation json files
// modules.hot.accept does not accept dynamic dependencies,
// have to be constants at compile-time
module.hot.accept(['./i18n', 'containers/App'], () => {
ReactDOM.unmountComponentAtNode(MOUNT_NODE);
render(translationMessages);
});
}
Related
I need to switch out my backend in-memory DB for testing due to memory issues. Below is my code
import { fireEvent, render, screen, waitFor } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import App from "App";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { AccessLevel, ResponseApi, SystemUserApi } from "types";
let mock: MockAdapter;
beforeAll(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.reset();
});
beforeEach(() => {
jest.resetModules();
});
describe("<App />", () => {
test("login", async () => {
mock.onPost('/Hello').reply(200, getPost);
const result = render(<App />);
const user = userEvent.setup();
const btnLogin = screen.getByText(/Login/i) as HTMLButtonElement;
await userEvent.click(btnLogin);
let btnOk = screen.queryByText(/OK/i) as HTMLButtonElement;
expect(btnOk.disabled).toBe(true);
let btnCancel = screen.getByText(/Cancel/i) as HTMLButtonElement;
expect(btnCancel.disabled).toBe(false);
fireEvent.change(screen.getByLabelText(/Access Code/i) as HTMLInputElement, { target: { value: 'USER' } });
expect(btnOk.disabled).toBe(false);
await userEvent.click(btnOk);
//At this point I was expecting the onPost to be clicked
});
});
function getPost(config: any): any {
console.log(config);
debugger;
return {
data: {
access_code: 'USER'.toUpperCase(),
access_level: AccessLevel.USER ,
lock_level:true
} as SystemUserApi,
error: false,
} as ResponseApi
}
Deep down in the is a call axios post to /Hello but my function within the test is not called. I do not know if it has to do with the actual call being axios.request vs axios.post. I have tried switching to mock.onAny, but that did not seem to work. Not sure what to do here.
I can't seem to get this simple test to work in react-testing-library & react-native-testing-library. I've tried various combinations of wrapping the render function in act, or using waitFor and other async utils, but the test never waits for the component to re-render after useEffect causes the async api call to set the new state.
Also worth noting I receive the warning: An update to TestComponent inside a test was not wrapped in act(...).`. I'm aware of this issue but no method that I've seen solved it for me.
import React, { useEffect, useState } from 'react'
import { View, Text } from 'react-native'
import { render, waitFor } from 'test-utils'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { useApi } from './index'
const server = setupServer(
rest.get('http://localhost/MOCK_VAR/some-endpoint', (req, res, ctx) => {
return res(ctx.json({ greeting: 'hello there' }))
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
function TestComponent() {
const { apiRequest } = useApi()
const [result, setResult] = useState(null)
useEffect(() => {
makeApiCall()
})
const makeApiCall = async () => {
const apiResult = await apiRequest({ url: '/some-endpoint' })
console.log(apiResult.greeting) // <-- 'hello there'
setResult(apiResult.greeting)
}
return (
<View>
<Text>{result}</Text>
</View>
)
}
describe('Test useApi hook', () => {
test('test post request', async () => {
const { findByText } = render(<TestComponent />)
const greeting = await findByText('hello there')
await waitFor(() => { // <-- never waits
expect(greeting).toBeTruthy()
})
})
})
My issue was awaiting the findBy function. From the docs it says findBy* methods have waitFor already built in. So simply removing the await solved the issue.
What worked for me:
test('test post request', async () => {
const { findByText } = render(<TestComponent />)
const greeting = findByText('hello there')
waitFor(() => expect(greeting).toBeTruthy())
})
I need to get the cookies value on the first render. I get those in _app.tsx.
Everything looks fine (render correctly the html) but I get the server/client mismatch warning because at the first render on Server, cookies are undefined and the value fall back to default value which is 0.
On hydration, the value is picked from cookies and is displayed correctly.
Could someone explain to me why is a problem that on the server the value is the default value (therefor why I get this warning) and what would be a better way to write this code?
Here my _app.tsx
import React, { useState, useEffect } from 'react'
import type { AppProps } from 'next/app'
import { UserContext } from '../context/UserContext'
require('es6-promise').polyfill()
let cookieHelper
if (typeof window !== 'undefined') {
cookieHelper = require( '../helpers/_cookies' ) // This is a file written by us where we export const and get/set cookies func
}
function ElliotApp ({ Component, pageProps }: AppProps) {
useEffect(() => {
import('../helpers/_cookies')
}, [])
const searchesFromCookies = cookieHelper?.get(cookieHelper?.COOKIE_NAME_SEARCH_COUNT) // this value is a string like '3'
const userState = {
numOfSearches: searchesFromCookies || 0
}
const [userContext, setUserContext] = useState(userState)
useEffect(() => {
cookieHelper?.set(cookieHelper?.COOKIE_NAME_SEARCH_COUNT, userContext.numOfSearches)
}, [userContext])
return (
<UserContext.Provider value={[userContext, setUserContext]}>
<Component {...pageProps} />
</UserContext.Provider>
)
}
export default ElliotApp
many thanks!
Could someone explain to me why is a problem that on the server the value is the default value
Probably because your cookieHelper is just reading cookies from document.cookie and there is no such thing on the server.
If you want to get cookie with SSR you could use getInitialProps:
function parseCookies(req) {
// cookie.parse is some function that accepts cookie string and return parsed object
return cookie.parse(req ? req.headers.cookie || "" : document.cookie)
}
ElliotApp.getInitialProps = async ({ req }) => {
const cookies = parseCookies(req)
return {
searchesFromCookies: cookies[COOKIE_NAME_SEARCH_COUNT]
}
}
and then do something with them in your App component:
function ElliotApp ({ Component, pageProps, searchesFromCookies }: AppProps) {
const userState = {
numOfSearches: searchesFromCookies || 0
}
const [userContext, setUserContext] = useState(userState)
// do whatever ...
}
EDIT:
In case you are fine with default value on the server then you just need to do everything inside useEffect hook (it wont run on the server):
function ElliotApp ({ Component, pageProps }: AppProps) {
const userState = {
numOfSearches: 0
}
const [userContext, setUserContext] = useState(userState)
useEffect(() => {
setUserContext({
numOfSearches: cookieHelper.get(cookieHelper.COOKIE_NAME_SEARCH_COUNT)
});
}, []);
useEffect(() => {
cookieHelper.set(cookieHelper.COOKIE_NAME_SEARCH_COUNT, userContext.numOfSearches)
}, [userContext])
return (
<UserContext.Provider value={[userContext, setUserContext]}>
<Component {...pageProps} />
</UserContext.Provider>
)
}
After 3 days researching and not ariving anywhere, I decided to ask here for someone that already have similar experience or can point a better path to follow.
The better SO question I've found was this but left some questions in air: React - how to test form submit?
Since I'm begginer I believe I may getting something wrong, but no sure exactly which. If it's the way I build the components or even test concept itself.
I have the following case:
When a user logins in, it calls API (mock) then save token result (when successful) to localStorage (mock)
When user is already logged in, it gets redirected to homepage
My code until now:
Login Component
class Login extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: ''
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleSubmit(e) {
e.preventDefault();
this.props.sendLoginRequest(this.state).then(
({data}) => {
console.log(data);
},
(data) => {
console.error(data);
}
);
}
handleChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
render() {
return (
<div id='auth-container' className='login'>
<Form onSubmit={this.handleSubmit}>
<FormGroup controlId='emailaddress'>
<InputGroup bsSize='large'>
<InputGroup.Addon>
<Icon glyph='icon-fontello-mail' />
</InputGroup.Addon>
<FormControl
autoFocus
className='border-focus-blue'
type='email'
placeholder='email#fixdin.com'
name='email'
onChange={this.handleChange}
value={this.state.email} />
</InputGroup>
</FormGroup>
<FormGroup controlId='password'>
<InputGroup bsSize='large'>
<InputGroup.Addon>
<Icon glyph='icon-fontello-key' />
</InputGroup.Addon>
<FormControl
className='border-focus-blue'
type='password'
placeholder='password'
name='password'
onChange={this.handleChange}
value={this.state.password} />
</InputGroup>
</FormGroup>
</Form>
</div>
)
}
}
Login.propTypes = {
sendLoginRequest: React.PropTypes.func.isRequired
}
authAction.js
import createApi from '../services/api';
import { saveToken } from '../services/session';
export function sendLoginRequest(loginData) {
return dispatch => {
const api = createApi();
const loginPromise = api.post('auth/', loginData);
loginPromise.then(
({ data }) => {
saveToken(data.token);
}
);
return loginPromise;
}
}
API..js
import axios from 'axios';
import { isAuthenticated, getToken } from './session';
export const BASE_URL = 'http://localhost:8000/api/v1/';
export default function createAPI() {
let auth = { }
if (isAuthenticated()) {
auth = {
Token: getToken()
}
}
return axios.create({
baseURL: BASE_URL,
auth: auth
});
};
session.js
const TOKEN_KEY = 'token';
export function saveToken(value)
{
localStorage.setItem(TOKEN_KEY, value);
}
export function getToken()
{
return localStorage.getItem(TOKEN_KEY)
}
export function isAuthenticated() {
return getToken() !== null;
}
My test stack is Mocha/Chai/Enzyme/sinon and it's defined
setup.js
var jsdom = require('jsdom');
class LocalStorageMock {
constructor() {
this.store = {};
}
clear() {
this.store = {};
}
getItem(key) {
return this.store[key];
}
setItem(key, value) {
this.store[key] = value.toString();
}
};
if(!global.document) {
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.window = document.defaultView;
global.navigator = {userAgent: 'node.js'};
global.localStorage = new LocalStorageMock;
}
login-test.js
import React from 'react';
import sinon from 'sinon';
import { mount, shallow } from 'enzyme';
import { expect } from 'chai';
import { Provider } from 'react-redux';
import axios from 'axios'
import moxios from 'moxios'
import store from './../src/store';
import LoginPage from './../src/auth/components/Login';
describe('Login', () => {
beforeEach(function () {
moxios.install(axios)
})
afterEach(function () {
moxios.uninstall(axios)
})
it('should call action on form submit', () => {
const submitRequest = sinon.stub(LoginPage.prototype, 'handleSubmit').returns(true);
const wrapper = mount(<Provider store={store}><LoginPage /></Provider>);
wrapper.find('form').simulate('submit');
expect(submitRequest.called).to.be.true;
submitRequest.restore();
});
it('should save token on succesfull login', () => {
const wrapper = mount(<Provider store={store}><LoginPage /></Provider>);
const emailInput = wrapper.find('input[type="email"]');
const passInput = wrapper.find('input[type="password"]');
const form = wrapper.find('form');
emailInput.value = "valid#email.com";
passInput.value = '123456789';
form.simulate('submit'); // Should I use submit button instead???
moxios.wait(function () {
let request = moxios.requests.mostRecent()
request.respondWith({
status: 200,
response:
{ Token: 'validToken' }
}).then(function () {
expect(localStorage.getItem('Token')).to.equal('validToken');
});
});
});
});
Above test does not pass, since it returns false for submitRequest.called and second test fails with error "Cannot read property 'respondWith' of undefined". I'm not sure how to fix and more, I'm not sure if I idealized it right!!
When doing a lot of research about it, I've seen examples with tests specific for component method call + isolated action test.
So...
When I think about "click login and save token" I'm overthinking a unit test? There's a better way to test things like that? Maybe separate some concerns?
This is the correctly way to test if a form submit invoke its callback? If so, why sinon is not working there?
This is the correctly way to mock + test api call to login and localStorage? If so, why Moxios is not working properly? It keeps giving me that mostRecent() is undefined.
If no, to question 2 and 3, where can I find a valid and working example of how to properly test cited behavior?
Thanks in advance.
There are a lot of different approaches to unit test your angular application you can find at the moment. A lot are already outdated and basically there's no real documentation at this point. So im really not sure which approach to use.
It seems a good approach at the moment is to use TestComponentBuilder, but i have some trouble to test parts of my code especially if a function on my component uses an injected service which returns an observable.
For example a basic Login Component with a Authentication Service (which uses a BackendService for the requests).
I leave out the templates here, because i don't want to test them with UnitTests (as far as i understood, TestComponentBuilder is pretty useful for this, but i just want to use a common approach for all my unit tests, and the it seems that TestComponentBuilder is supposed to handle every testable aspect, please correct me if i'm wrong here)
So i got my LoginComponent:
export class LoginComponent {
user:User;
isLoggingIn:boolean;
errorMessage:string;
username:string;
password:string;
constructor(private _authService:AuthService, private _router:Router) {
this._authService.isLoggedIn().subscribe(isLoggedIn => {
if(isLoggedIn) {
this._router.navigateByUrl('/anotherView');
}
});
}
login():any {
this.errorMessage = null;
this.isLoggingIn = true;
this._authService.login(this.username, this.password)
.subscribe(
user => {
this.user = user;
setTimeout(() => {
this._router.navigateByUrl('/anotherView');
}, 2000);
},
errorMessage => {
this.password = '';
this.errorMessage = errorMessage;
this.isLoggingIn = false;
}
);
}
}
My AuthService:
#Injectable()
export class AuthService {
private _user:User;
private _urls:any = {
...
};
constructor( private _backendService:BackendService,
#Inject(APP_CONFIG) private _config:Config,
private _localStorage:LocalstorageService,
private _router:Router) {
this._user = _localStorage.get(LOCALSTORAGE_KEYS.CURRENT_USER);
}
get user():User {
return this._user || this._localStorage.get(LOCALSTORAGE_KEYS.CURRENT_USER);
}
set user(user:User) {
this._user = user;
if (user) {
this._localStorage.set(LOCALSTORAGE_KEYS.CURRENT_USER, user);
} else {
this._localStorage.remove(LOCALSTORAGE_KEYS.CURRENT_USER);
}
}
isLoggedIn (): Observable<boolean> {
return this._backendService.get(this._config.apiUrl + this._urls.isLoggedIn)
.map(response => {
return !(!response || !response.IsUserAuthenticated);
});
}
login (username:string, password:string): Observable<User> {
let body = JSON.stringify({username, password});
return this._backendService.post(this._config.apiUrl + this._urls.login, body)
.map(() => {
this.user = new User(username);
return this.user;
});
}
logout ():Observable<any> {
return this._backendService.get(this._config.apiUrl + this._urls.logout)
.map(() => {
this.user = null;
this._router.navigateByUrl('/login');
return true;
});
}
}
and finally my BackendService:
#Injectable()
export class BackendService {
_lastErrorCode:number;
private _errorCodes = {
...
};
constructor( private _http:Http, private _router:Router) {
}
post(url:string, body:any):Observable<any> {
let options = new RequestOptions();
this._lastErrorCode = 0;
return this._http.post(url, body, options)
.map((response:any) => {
...
return body.Data;
})
.catch(this._handleError);
}
...
private _handleError(error:any) {
...
let errMsg = error.message || 'Server error';
return Observable.throw(errMsg);
}
}
Now i want to test the basic logic of logging in, one time it should fail and i expect an error message (which is thrown by my BackendService in its handleError function) and in another test it should login and set my User-object
This is my current approach for my Login.component.spec:
Updated: added fakeAsync like suggested in Günters answer.
export function main() {
describe('Login', () => {
beforeEachProviders(() => [
ROUTER_FAKE_PROVIDERS
]);
it('should try and fail logging in',
inject([TestComponentBuilder], fakeAsync((tcb: TestComponentBuilder) => {
tcb.createAsync(TestComponent)
.then((fixture: any) => {
tick();
fixture.detectChanges();
let loginInstance = fixture.debugElement.children[0].componentInstance;
expect(loginInstance.errorMessage).toBeUndefined();
loginInstance.login();
tick();
fixture.detectChanges();
expect(loginInstance.isLoggingIn).toBe(true);
fixture.detectChanges();
expect(loginInstance.isLoggingIn).toBe(false);
expect(loginInstance.errorMessage.length).toBeGreaterThan(0);
});
})));
it('should log in',
inject([TestComponentBuilder], fakeAsync((tcb: TestComponentBuilder) => {
tcb.createAsync(TestComponent)
.then((fixture: any) => {
tick();
fixture.detectChanges();
let loginInstance = fixture.debugElement.children[0].componentInstance;
loginInstance.username = 'abc';
loginInstance.password = '123';
loginInstance.login();
tick();
fixture.detectChanges();
expect(loginInstance.isLoggingIn).toBe(true);
expect(loginInstance.user).toEqual(jasmine.any(User));
});
})));
});
}
#Component({
selector: 'test-cmp',
template: `<my-login></my-login>`,
directives: [LoginComponent],
providers: [
HTTP_PROVIDERS,
provide(APP_CONFIG, {useValue: CONFIG}),
LocalstorageService,
BackendService,
AuthService,
BaseRequestOptions,
MockBackend,
provide(Http, {
useFactory: function(backend:ConnectionBackend, defaultOptions:BaseRequestOptions) {
return new Http(backend, defaultOptions);
},
deps: [MockBackend, BaseRequestOptions]
})
]
})
class TestComponent {
}
There are several issues with this test.
ERROR: 'Unhandled Promise rejection:', 'Cannot read property 'length' of null' I get this for the test of `loginInstance.errorMessage.length
Expected true to be false. in the first test after i called login
Expected undefined to equal <jasmine.any(User)>. in the second test after it should have logged in.
Any hints how to solve this? Am i using a wrong approach here?
Any help would be really appreciated (and im sorry for the wall of text / code ;) )
As you can't know when this._authService.login(this.username, this.password).subscribe( ... ) is actually called you can't just continue the test synchronically and assume the subscribe callback has happened. In fact it can't yet have happened because sync code (your test) is executed to the end first.
You can add artificial delays (ugly and flaky)
You can provide observables or promises in your component that emit/resolve when something you want to test is actually done (ugly because test code added to production code)
I guess the best option is using fakeAsync which provides more control about async execution during tests (I haven't used it myself)
As far as I know there will come support in Angular tests using zone, to wait for the async queue to become empty before the test continues (I don't know details about this neither).