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:
parent
a5330bc08f
commit
f8698d0293
19 changed files with 201 additions and 42 deletions
26
app/components/__snapshots__/formatted_time.test.js.snap
Normal file
26
app/components/__snapshots__/formatted_time.test.js.snap
Normal 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>
|
||||
`;
|
||||
|
|
@ -13,7 +13,7 @@ exports[`Swiper should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<ScrollViewMock
|
||||
<ScrollView
|
||||
automaticallyAdjustContentInsets={true}
|
||||
bounces={false}
|
||||
contentContainerStyle={
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
97
app/components/formatted_time.test.js
Normal file
97
app/components/formatted_time.test.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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
19
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue