MM-21342 Fix leading zero on post by using react-intl by default and fallback to moment-timezone for unsupported locale (#3797)

* used react-intl for post time as default and moment-timezone as fallback

* add @testing-library/react-native to better test component rendering
This commit is contained in:
Saturnino Abril 2020-01-14 07:12:51 +08:00 committed by Elias Nahum
parent a5330bc08f
commit f8698d0293
19 changed files with 201 additions and 42 deletions

View file

@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormattedTime should render correctly 1`] = `
<View
pointerEvents="box-none"
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={true}
pointerEvents="box-none"
style={
Object {
"flex": 1,
}
}
>
<Text>
7:02 PM
</Text>
</View>
</View>
`;

View file

@ -13,7 +13,7 @@ exports[`Swiper should match snapshot 1`] = `
]
}
>
<ScrollViewMock
<ScrollView
automaticallyAdjustContentInsets={true}
bounces={false}
contentContainerStyle={

View file

@ -3,7 +3,7 @@
exports[`EditChannelInfo should match snapshot 1`] = `
<React.Fragment>
<Connect(StatusBar) />
<KeyboardAwareScrollViewMock
<KeyboardAwareScrollView
enableAutomaticScroll={true}
enableOnAndroid={false}
enableResetScrollToCoords={true}
@ -319,6 +319,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
</View>
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollViewMock>
</KeyboardAwareScrollView>
</React.Fragment>
`;

View file

@ -4,17 +4,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Text} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import moment from 'moment-timezone';
import CustomPropTypes from 'app/constants/custom_prop_types';
export default class FormattedTime extends React.PureComponent {
class FormattedTime extends React.PureComponent {
static propTypes = {
value: PropTypes.any.isRequired,
timeZone: PropTypes.string,
children: PropTypes.func,
hour12: PropTypes.bool,
style: CustomPropTypes.Style,
intl: intlShape.isRequired,
};
getFormattedTime = () => {
@ -22,8 +24,22 @@ export default class FormattedTime extends React.PureComponent {
value,
timeZone,
hour12,
intl,
} = this.props;
const timezoneProps = timeZone ? {timeZone} : {};
const options = {
...timezoneProps,
hour12,
};
const formattedTime = intl.formatTime(value, options);
// `formatTime` returns unformatted date string on error like in the case of (react-intl) unsupported timezone.
// Therefore, use react-intl by default or moment-timezone for unsupported timezone.
if (formattedTime !== String(new Date(value))) {
return formattedTime;
}
const format = hour12 ? 'hh:mm A' : 'HH:mm';
if (timeZone) {
return moment.tz(value, timeZone).format(format);
@ -43,3 +59,5 @@ export default class FormattedTime extends React.PureComponent {
return <Text style={style}>{formattedTime}</Text>;
}
}
export default injectIntl(FormattedTime);

View file

@ -0,0 +1,97 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {render} from '@testing-library/react-native';
import {IntlProvider} from 'react-intl';
import IntlPolyfill from 'intl';
import 'intl/locale-data/jsonp/es';
import 'intl/locale-data/jsonp/ko';
import FormattedTime from './formatted_time';
describe('FormattedTime', () => {
const baseProps = {
value: 1548788533405,
timeZone: 'UTC',
hour12: true,
};
setupTest();
it('should render correctly', () => {
console.error = jest.fn();
let wrapper = renderWithIntl(
<FormattedTime {...baseProps}/>
);
expect(wrapper.baseElement).toMatchSnapshot();
expect(wrapper.getByText('7:02 PM')).toBeTruthy();
wrapper = renderWithIntl(
<FormattedTime
{...baseProps}
hour12={false}
/>
);
expect(wrapper.getByText('19:02')).toBeTruthy();
});
it('should support localization', () => {
let wrapper = renderWithIntl(
<FormattedTime {...baseProps}/>,
'es',
);
expect(wrapper.getByText('7:02 p. m.')).toBeTruthy();
wrapper = renderWithIntl(
<FormattedTime {...baseProps}/>,
'ko',
);
expect(wrapper.getByText('오후 7:02')).toBeTruthy();
wrapper = renderWithIntl(
<FormattedTime
{...baseProps}
hour12={false}
/>,
'ko',
);
expect(wrapper.getByText('19:02')).toBeTruthy();
});
it('should fallback to default short format for unsupported locale of react-intl ', () => {
let wrapper = renderWithIntl(
<FormattedTime
{...baseProps}
timeZone='NZ-CHAT'
/>,
'es',
);
expect(wrapper.getByText('08:47 AM')).toBeTruthy();
wrapper = renderWithIntl(
<FormattedTime
{...baseProps}
timeZone='NZ-CHAT'
hour12={false}
/>
);
expect(wrapper.getByText('08:47')).toBeTruthy();
});
});
function renderWithIntl(component, locale = 'en') {
return render(<IntlProvider locale={locale}>{component}</IntlProvider>);
}
function setupTest() {
global.Intl = IntlPolyfill;
}

View file

@ -10,7 +10,7 @@ exports[`MarkdownTable should match snapshot 1`] = `
}
type="opacity"
>
<ScrollViewMock
<ScrollView
onContentSizeChange={[Function]}
onLayout={[Function]}
scrollEnabled={false}
@ -132,7 +132,7 @@ exports[`MarkdownTable should match snapshot 1`] = `
/>
</>
</View>
</ScrollViewMock>
</ScrollView>
<LinearGradient
colors={
Array [

View file

@ -51,7 +51,7 @@ exports[`PostHeader should match snapshot when just a base post 1`] = `
John Smith
</Text>
</TouchableWithFeedbackIOS>
<FormattedTime
<InjectIntl(FormattedTime)
hour12={true}
style={
Object {
@ -123,7 +123,7 @@ exports[`PostHeader should match snapshot when just a base post in landscape mod
John Smith
</Text>
</TouchableWithFeedbackIOS>
<FormattedTime
<InjectIntl(FormattedTime)
hour12={true}
style={
Object {
@ -232,7 +232,7 @@ exports[`PostHeader should match snapshot when post is autoresponder 1`] = `
}
}
/>
<FormattedTime
<InjectIntl(FormattedTime)
hour12={true}
style={
Object {
@ -297,7 +297,7 @@ exports[`PostHeader should match snapshot when post is from system message 1`] =
}
/>
</View>
<FormattedTime
<InjectIntl(FormattedTime)
hour12={true}
style={
Object {
@ -367,7 +367,7 @@ exports[`PostHeader should match snapshot when post is same thread, so dont disp
John Smith
</Text>
</TouchableWithFeedbackIOS>
<FormattedTime
<InjectIntl(FormattedTime)
hour12={true}
style={
Object {
@ -494,7 +494,7 @@ exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyB
}
}
/>
<FormattedTime
<InjectIntl(FormattedTime)
hour12={true}
style={
Object {
@ -646,7 +646,7 @@ exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyB
}
}
/>
<FormattedTime
<InjectIntl(FormattedTime)
hour12={true}
style={
Object {
@ -758,7 +758,7 @@ exports[`PostHeader should match snapshot when post renders Commented On for new
John Smith
</Text>
</TouchableWithFeedbackIOS>
<FormattedTime
<InjectIntl(FormattedTime)
hour12={true}
style={
Object {
@ -845,7 +845,7 @@ exports[`PostHeader should match snapshot when post should display reply button
John Smith
</Text>
</TouchableWithFeedbackIOS>
<FormattedTime
<InjectIntl(FormattedTime)
hour12={true}
style={
Object {

View file

@ -42,7 +42,7 @@ exports[`PostList setting channel deep link 1`] = `
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
refreshControl={
<RefreshControlMock
<RefreshControl
colors={
Array [
"#3d3c40",
@ -103,7 +103,7 @@ exports[`PostList setting permalink deep link 1`] = `
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
refreshControl={
<RefreshControlMock
<RefreshControl
colors={
Array [
"#3d3c40",
@ -164,7 +164,7 @@ exports[`PostList should match snapshot 1`] = `
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
refreshControl={
<RefreshControlMock
<RefreshControl
colors={
Array [
"#3d3c40",

View file

@ -10,7 +10,7 @@ exports[`channel_info should match snapshot 1`] = `
}
>
<Connect(StatusBar) />
<ScrollViewMock
<ScrollView
style={
Object {
"backgroundColor": undefined,
@ -577,6 +577,6 @@ exports[`channel_info should match snapshot 1`] = `
togglable={false}
/>
</View>
</ScrollViewMock>
</ScrollView>
</View>
`;

View file

@ -8,7 +8,7 @@ exports[`InteractiveDialog should display introduction text if present 1`] = `
}
}
>
<ScrollViewMock
<ScrollView
style={
Object {
"marginBottom": 20,
@ -50,6 +50,6 @@ exports[`InteractiveDialog should display introduction text if present 1`] = `
}
value="**Some** _introduction_ text"
/>
</ScrollViewMock>
</ScrollView>
</View>
`;

View file

@ -318,7 +318,7 @@ LongPost {
/>
</View>
</View>
<ScrollViewMock
<ScrollView
style={
Object {
"backgroundColor": "#ffffff",
@ -334,7 +334,7 @@ LongPost {
showAddReaction={false}
showLongPost={true}
/>
</ScrollViewMock>
</ScrollView>
<View
style={
Object {

View file

@ -13,7 +13,7 @@ exports[`ReactionHeader should match snapshot 1`] = `
}
}
>
<ScrollViewMock
<ScrollView
alwaysBounceHorizontal={false}
horizontal={true}
overScrollMode="never"
@ -89,7 +89,7 @@ exports[`ReactionHeader should match snapshot 1`] = `
}
}
/>
</ScrollViewMock>
</ScrollView>
</AnimatedComponent>
</NativeViewGestureHandler>
`;

View file

@ -239,7 +239,7 @@ NotificationSettings {
}
>
<Connect(StatusBar) />
<ScrollViewMock
<ScrollView
alwaysBounceVertical={false}
contentContainerStyle={
Object {
@ -386,7 +386,7 @@ NotificationSettings {
}
}
/>
</ScrollViewMock>
</ScrollView>
</View>,
"_rendering": false,
"_updater": [Circular],

View file

@ -49,7 +49,7 @@ exports[`NotificationSettingsEmailAndroid should match snapshot 1`] = `
`;
exports[`NotificationSettingsEmailAndroid should match snapshot 2`] = `
<Component
<Modal
animationType="slide"
hardwareAccelerated={false}
onRequestClose={[Function]}
@ -140,5 +140,5 @@ exports[`NotificationSettingsEmailAndroid should match snapshot 2`] = `
</View>
</View>
</View>
</Component>
</Modal>
`;

View file

@ -167,7 +167,7 @@ NotificationSettingsMentionsKeywords {
}
>
<Connect(StatusBar) />
<ScrollViewMock
<ScrollView
alwaysBounceVertical={false}
contentContainerStyle={
Object {
@ -243,7 +243,7 @@ NotificationSettingsMentionsKeywords {
}
/>
</View>
</ScrollViewMock>
</ScrollView>
</View>,
"_rendering": false,
"_updater": [Circular],

View file

@ -3,7 +3,7 @@
exports[`TermsOfService should enable/disable navigator buttons on setNavigatorButtons true/false 1`] = `
<React.Fragment>
<Connect(StatusBar) />
<ScrollViewMock
<ScrollView
contentContainerStyle={
Object {
"paddingBottom": 50,
@ -131,14 +131,14 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB
}
value="Terms Text"
/>
</ScrollViewMock>
</ScrollView>
</React.Fragment>
`;
exports[`TermsOfService should enable/disable navigator buttons on setNavigatorButtons true/false 2`] = `
<React.Fragment>
<Connect(StatusBar) />
<ScrollViewMock
<ScrollView
contentContainerStyle={
Object {
"paddingBottom": 50,
@ -266,7 +266,7 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB
}
value="Terms Text"
/>
</ScrollViewMock>
</ScrollView>
</React.Fragment>
`;
@ -338,7 +338,7 @@ exports[`TermsOfService should match snapshot for get terms 1`] = `
exports[`TermsOfService should match snapshot on enableNavigatorLogout 1`] = `
<React.Fragment>
<Connect(StatusBar) />
<ScrollViewMock
<ScrollView
contentContainerStyle={
Object {
"paddingBottom": 50,
@ -466,6 +466,6 @@ exports[`TermsOfService should match snapshot on enableNavigatorLogout 1`] = `
}
value="Terms Text"
/>
</ScrollViewMock>
</ScrollView>
</React.Fragment>
`;

View file

@ -9,7 +9,7 @@ exports[`user_profile should match snapshot 1`] = `
}
>
<Connect(StatusBar) />
<ScrollViewMock
<ScrollView
style={
Object {
"backgroundColor": "#ffffff",
@ -240,6 +240,6 @@ exports[`user_profile should match snapshot 1`] = `
}
togglable={false}
/>
</ScrollViewMock>
</ScrollView>
</View>
`;

19
package-lock.json generated
View file

@ -1487,6 +1487,16 @@
"yargs": "^12.0.2"
}
},
"@testing-library/react-native": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-5.0.3.tgz",
"integrity": "sha512-lQH7vUgwESfagFw4BlKsfpX7Rv/m7h2NYfubY0aoQromSwI1slCxrhwZws8gABTXweob/DyLATsOamHsWdwDnA==",
"dev": true,
"requires": {
"pretty-format": "^24.9.0",
"wait-for-expect": "^3.0.0"
}
},
"@types/babel__core": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz",
@ -5500,7 +5510,8 @@
"intl": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/intl/-/intl-1.2.5.tgz",
"integrity": "sha1-giRKIZDE5Bn4Nx9ao02qNCDiq94="
"integrity": "sha1-giRKIZDE5Bn4Nx9ao02qNCDiq94=",
"dev": true
},
"intl-format-cache": {
"version": "2.2.9",
@ -12498,6 +12509,12 @@
"browser-process-hrtime": "^0.1.2"
}
},
"wait-for-expect": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.1.tgz",
"integrity": "sha512-3Ha7lu+zshEG/CeHdcpmQsZnnZpPj/UsG3DuKO8FskjuDbkx3jE3845H+CuwZjA2YWYDfKMU2KhnCaXMLd3wVw==",
"dev": true
},
"walker": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",

View file

@ -87,6 +87,7 @@
"@babel/plugin-transform-runtime": "7.6.2",
"@babel/preset-env": "7.6.3",
"@babel/register": "7.6.2",
"@testing-library/react-native": "5.0.3",
"babel-eslint": "10.0.3",
"babel-jest": "24.9.0",
"babel-plugin-module-resolver": "3.2.0",
@ -139,7 +140,7 @@
"lcov",
"text-summary"
],
"preset": "react-native",
"preset": "@testing-library/react-native",
"setupFilesAfterEnv": [
"<rootDir>/test/setup.js",
"<rootDir>/node_modules/jest-enzyme/lib/index.js"