Skip to content

Commit 9e27cb1

Browse files
authored
feat: improve, document module testability (#587)
* feat: prep for improving module testability * fix tests * tests, docs * docs * add link to examples * fix ci checks * fix TOC
1 parent 737ab1a commit 9e27cb1

21 files changed

+800
-698
lines changed

README.md

+29-25
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ React Native date & time picker component for iOS, Android and Windows.
8383
- [`style` (`optional`, `iOS only`)](#style-optional-ios-only)
8484
- [`disabled` (`optional`, `iOS only`)](#disabled-optional-ios-only)
8585
- [`onError` (`optional`, `Android only`)](#onError-optional-android-only)
86+
- [Testing with Jest](#testing-with-jest)
8687
- [Migration from the older components](#migration-from-the-older-components)
8788
- [Contributing to the component](#contributing-to-the-component)
8889
- [Manual installation](#manual-installation)
@@ -146,8 +147,8 @@ export const App = () => {
146147
value: date,
147148
onChange,
148149
mode: currentMode,
149-
is24Hour: true
150-
})
150+
is24Hour: true,
151+
});
151152
};
152153

153154
const showDatepicker = () => {
@@ -160,16 +161,12 @@ export const App = () => {
160161

161162
return (
162163
<View>
163-
<View>
164-
<Button onPress={showDatepicker} title="Show date picker!" />
165-
</View>
166-
<View>
167-
<Button onPress={showTimepicker} title="Show time picker!" />
168-
</View>
164+
<Button onPress={showDatepicker} title="Show date picker!" />
165+
<Button onPress={showTimepicker} title="Show time picker!" />
169166
<Text>selected: {date.toLocaleString()}</Text>
170167
</View>
171168
);
172-
}
169+
};
173170
```
174171

175172
### Component usage on iOS / Android / Windows
@@ -187,7 +184,10 @@ export const App = () => {
187184
};
188185

189186
const showMode = (currentMode) => {
190-
setShow(true);
187+
if (Platform.OS === 'android') {
188+
setShow(false);
189+
// for iOS, add a button that closes the picker
190+
}
191191
setMode(currentMode);
192192
};
193193

@@ -201,12 +201,8 @@ export const App = () => {
201201

202202
return (
203203
<View>
204-
<View>
205-
<Button onPress={showDatepicker} title="Show date picker!" />
206-
</View>
207-
<View>
208-
<Button onPress={showTimepicker} title="Show time picker!" />
209-
</View>
204+
<Button onPress={showDatepicker} title="Show date picker!" />
205+
<Button onPress={showTimepicker} title="Show time picker!" />
210206
<Text>selected: {date.toLocaleString()}</Text>
211207
{show && (
212208
<DateTimePicker
@@ -219,12 +215,11 @@ export const App = () => {
219215
)}
220216
</View>
221217
);
222-
}
218+
};
223219
```
224220

225221
</details>
226222

227-
228223
## Localization note
229224

230225
By localization, we refer to the language (names of months and days), as well as order in which date can be presented in a picker (month/day vs. day/month) and 12 / 24 hour-format.
@@ -241,16 +236,15 @@ There is also the iOS-only locale prop that can be used to force locale in some
241236

242237
For Expo, follow the [localization docs](https://docs.expo.dev/distribution/app-stores/#localizing-your-ios-app).
243238

244-
245239
### Android imperative api
246240

247241
On Android, you have a choice between using the component API (regular React component) or an imperative api (think of something like `ReactNative.alert()`).
248242

249-
While the component API has the benefit of writing the same code on all platforms, to start we recommend using the imperative API on Android.
243+
While the component API has the benefit of writing the same code on all platforms, for start we recommend using the imperative API on Android.
250244

251245
The `params` is an object with the same properties as the component props documented in the next paragraph. (This is also because the component api internally uses the imperative one.)
252246

253-
```js
247+
```ts
254248
import { DateTimePickerAndroid } from '@react-native-community/datetimepicker';
255249

256250
DateTimePickerAndroid.open(params: AndroidNativeProps)
@@ -294,7 +288,7 @@ List of possible values for Android
294288
List of possible values for iOS (maps to [preferredDatePickerStyle](https://developer.apple.com/documentation/uikit/uidatepicker/3526124-preferreddatepickerstyle?changes=latest_minor&language=objc))
295289

296290
- `"default"` - Automatically pick the best style available for the current platform & mode.
297-
- `"spinner"` - the usual appearance with a wheel from which you choose values
291+
- `"spinner"` - the usual pre-iOS 14 appearance with a wheel from which you choose values
298292
- `"compact"` - Affects only iOS 14 and later. Will fall back to "spinner" if not supported.
299293
- `"inline"` - Affects only iOS 14 and later. Will fall back to "spinner" if not supported.
300294

@@ -350,7 +344,7 @@ We strongly recommend avoiding this prop on android because of known issues in t
350344

351345
#### `timeZoneOffsetInSeconds` (`optional`, `Windows only`)
352346

353-
Allows changing of the time zone of the date picker. By default it uses the device's time zone.
347+
Allows changing of the time zone of the date picker. By default, it uses the device's time zone.
354348

355349
```js
356350
// UTC+1
@@ -423,7 +417,7 @@ Prefer localization as documented in [Localization note](#localization-note).
423417

424418
#### `is24Hour` (`optional`, `Windows and Android only`)
425419

426-
Allows changing of the time picker to a 24 hour format. By default, this value is decided automatcially based on the locale and other preferences.
420+
Allows changing of the time picker to a 24-hour format. By default, this value is decided automatically based on the locale and other preferences.
427421

428422
```js
429423
<RNDateTimePicker is24Hour={true} />
@@ -473,7 +467,7 @@ Sets style directly on picker component. By default, the picker height is determ
473467

474468
Please note that by default, picker's text color is controlled by the application theme (light / dark mode). In dark mode, text is white and in light mode, text is black.
475469

476-
This means that eg. if the device has dark mode turned on, and your screen background color is white, you will not see the picker. Please use the `Appearance` api to adjust the picker's background color so that it is visible, as we do in the [example App](/example/App.js).
470+
This means that e.g. if the device has dark mode turned on, and your screen background color is white, you will not see the picker. Please use the `Appearance` api to adjust the picker's background color so that it is visible, as we do in the [example App](/example/App.js).
477471
Alternatively, use the `themeVariant` prop or [opt-out from dark mode (discouraged)](https://stackoverflow.com/a/56546554/2070942).
478472

479473
```js
@@ -488,6 +482,16 @@ If true, the user won't be able to interact with the view.
488482

489483
Callback that is called when an error occurs inside the date picker native code (such as null activity).
490484

485+
## Testing with Jest
486+
487+
If you're rendering the picker component (using the `@testing-library/react-native` or similar), you need to mock the native module:
488+
489+
```
490+
"setupFiles": ["./node_modules/@react-native-community/datetimepicker/jest/setup.js"]
491+
```
492+
493+
For examples of how you can write your tests, look [here](/test/userlandTestExamples.test.js).
494+
491495
## Migration from the older components
492496

493497
Please see [migration.md](/docs/migration.md)

example/App.js

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export const App = () => {
9999
if (Platform.OS === 'android') {
100100
setShow(false);
101101
}
102+
102103
if (event.type === 'neutralButtonPressed') {
103104
setDate(new Date(0));
104105
} else {

jest.config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ module.exports = {
44
transform: {
55
'^.+\\.js$': require.resolve('react-native/jest/preprocessor.js'),
66
},
7-
setupFiles: ['./test/setup.js'],
7+
setupFiles: ['./jest/setup.js'],
8+
testRegex: '(/__tests__/.*|(\\.|/)(test))\\.[jt]sx?$',
89
};

jest/index.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @format
3+
* @flow strict-local
4+
*/
5+
import * as androidUtils from '../src/androidUtils';
6+
import {DATE_SET_ACTION, DISMISS_ACTION} from '../src/constants';
7+
import type {PresentPickerCallback} from '../src/androidUtils';
8+
9+
export const mockAndroidDialogDateChange = (datePickedByUser: Date) => {
10+
jest.spyOn(androidUtils, 'getOpenPicker').mockImplementation(() => {
11+
async function fakeDateTimePickerAndroidOpener({
12+
value: timestampFromPickerValueProp,
13+
}) {
14+
const pickedDate = new Date(timestampFromPickerValueProp);
15+
pickedDate.setFullYear(
16+
datePickedByUser.getFullYear(),
17+
datePickedByUser.getMonth(),
18+
datePickedByUser.getDate(),
19+
);
20+
21+
return {
22+
action: DATE_SET_ACTION,
23+
year: pickedDate.getFullYear(),
24+
month: pickedDate.getMonth(),
25+
day: pickedDate.getDate(),
26+
hour: pickedDate.getHours(),
27+
minute: pickedDate.getMinutes(),
28+
};
29+
}
30+
return (fakeDateTimePickerAndroidOpener: PresentPickerCallback);
31+
});
32+
};
33+
34+
export const mockAndroidDialogDismissal = () => {
35+
jest.spyOn(androidUtils, 'getOpenPicker').mockImplementation(() => {
36+
async function fakeDateTimePickerAndroidOpener() {
37+
return {
38+
action: DISMISS_ACTION,
39+
};
40+
}
41+
// $FlowExpectedError - the typings actually don't 100% reflect the native module behavior
42+
return (fakeDateTimePickerAndroidOpener: PresentPickerCallback);
43+
});
44+
};

jest/setup.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {NativeModules} from 'react-native';
2+
NativeModules.RNDateTimePickerManager = {
3+
getDefaultDisplayValue: jest.fn(() =>
4+
Promise.resolve({
5+
determinedDisplayValue: 'spinner',
6+
}),
7+
),
8+
};

package.json

+13-9
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
"src",
1111
"flow-typed",
1212
"windows",
13-
"RNDateTimePicker.podspec"
13+
"RNDateTimePicker.podspec",
14+
"!android/build",
15+
"!ios/build",
16+
"!**/__tests__",
17+
"!**/__fixtures__",
18+
"!**/__mocks__"
1419
],
1520
"publishConfig": {
1621
"access": "public"
@@ -20,8 +25,7 @@
2025
"start:android": "react-native run-android --no-jetifier",
2126
"start:ios": "react-native run-ios --project-path example/ios",
2227
"start:windows": "react-native start --use-react-native-windows",
23-
"test": "jest ./test",
24-
"posttest": "npm run lint",
28+
"test": "jest",
2529
"lint": "eslint {example,src,test}/**/*.js src/index.d.ts",
2630
"flow": "flow check",
2731
"detox:ios:build:debug": "detox build -c ios.sim.debug",
@@ -60,14 +64,14 @@
6064
},
6165
"homepage": "https://github.com/react-native-community/datetimepicker#readme",
6266
"devDependencies": {
63-
"@babel/core": "^7.12.9",
64-
"@babel/runtime": "^7.12.5",
67+
"@babel/core": "^7.17.8",
68+
"@babel/runtime": "^7.17.8",
6569
"@react-native-community/eslint-config": "^2.0.0",
6670
"@react-native-segmented-control/segmented-control": "^2.4.0",
6771
"@semantic-release/git": "^10.0.1",
68-
"@testing-library/react-native": "^8.0.0",
69-
"babel-jest": "^27.4.4",
70-
"detox": "19.5.3",
72+
"@testing-library/react-native": "^9.0.0",
73+
"babel-jest": "^27.5.1",
74+
"detox": "19.5.7",
7175
"eslint": "^7",
7276
"eslint-plugin-prettier": "^4.0.0",
7377
"flow-bin": "^0.158.0",
@@ -78,7 +82,7 @@
7882
"moment": "^2.24.0",
7983
"patch-package": "^6.4.7",
8084
"postinstall-postinstall": "^2.1.0",
81-
"prettier": "^2.5.1",
85+
"prettier": "^2.6.0",
8286
"react": "17.0.2",
8387
"react-native": "^0.66.4",
8488
"react-native-localize": "^2.2.0",

src/DateTimePickerAndroid.android.js

+31-33
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@ import {
99
NEUTRAL_BUTTON_ACTION,
1010
ANDROID_DISPLAY,
1111
ANDROID_MODE,
12-
ANDROID_EVT_TYPE,
13-
EVENT_TYPE_SET,
1412
} from './constants';
1513
import invariant from 'invariant';
1614

17-
import type {DateTimePickerEvent, AndroidNativeProps} from './types';
15+
import type {AndroidNativeProps} from './types';
1816
import {
1917
getOpenPicker,
2018
timeZoneOffsetDateSetter,
2119
validateAndroidProps,
2220
} from './androidUtils';
2321
import pickers from './picker';
22+
import {
23+
createDateTimeSetEvtParams,
24+
createDismissEvtParams,
25+
createNeutralEvtParams,
26+
} from './eventCreators';
2427

2528
function open(props: AndroidNativeProps) {
2629
const {
@@ -30,9 +33,7 @@ function open(props: AndroidNativeProps) {
3033
is24Hour,
3134
minimumDate,
3235
maximumDate,
33-
positiveButtonLabel,
3436
neutralButtonLabel,
35-
negativeButtonLabel,
3637
minuteInterval,
3738
timeZoneOffsetInMinutes,
3839
onChange,
@@ -42,54 +43,51 @@ function open(props: AndroidNativeProps) {
4243
invariant(originalValue, 'A date or time must be specified as `value` prop.');
4344

4445
const valueTimestamp = originalValue.getTime();
45-
const openPicker = getOpenPicker({
46-
mode,
47-
value: valueTimestamp,
48-
display,
49-
is24Hour,
50-
minimumDate,
51-
maximumDate,
52-
minuteInterval,
53-
timeZoneOffsetInMinutes,
54-
positiveButtonLabel,
55-
neutralButtonLabel,
56-
negativeButtonLabel,
57-
});
46+
const openPicker = getOpenPicker(mode);
5847

5948
const presentPicker = async () => {
6049
try {
61-
const {action, day, month, year, minute, hour} = await openPicker();
62-
let date = new Date(valueTimestamp);
63-
let event: DateTimePickerEvent = {
64-
type: EVENT_TYPE_SET,
65-
nativeEvent: {},
66-
};
50+
const {action, day, month, year, minute, hour} = await openPicker({
51+
value: valueTimestamp,
52+
display,
53+
is24Hour,
54+
minimumDate,
55+
maximumDate,
56+
neutralButtonLabel,
57+
minuteInterval,
58+
timeZoneOffsetInMinutes,
59+
});
6760

6861
switch (action) {
69-
case DATE_SET_ACTION:
62+
case DATE_SET_ACTION: {
63+
let date = new Date(valueTimestamp);
7064
date.setFullYear(year, month, day);
7165
date = timeZoneOffsetDateSetter(date, timeZoneOffsetInMinutes);
72-
event.nativeEvent.timestamp = date.getTime();
66+
const [event] = createDateTimeSetEvtParams(date);
7367
onChange?.(event, date);
7468
break;
69+
}
7570

76-
case TIME_SET_ACTION:
71+
case TIME_SET_ACTION: {
72+
let date = new Date(valueTimestamp);
7773
date.setHours(hour, minute);
7874
date = timeZoneOffsetDateSetter(date, timeZoneOffsetInMinutes);
79-
event.nativeEvent.timestamp = date.getTime();
75+
const [event] = createDateTimeSetEvtParams(date);
8076
onChange?.(event, date);
8177
break;
78+
}
8279

83-
case NEUTRAL_BUTTON_ACTION:
84-
event.type = ANDROID_EVT_TYPE.neutralButtonPressed;
80+
case NEUTRAL_BUTTON_ACTION: {
81+
const [event] = createNeutralEvtParams(originalValue);
8582
onChange?.(event, originalValue);
8683
break;
87-
84+
}
8885
case DISMISS_ACTION:
89-
default:
90-
event.type = ANDROID_EVT_TYPE.dismissed;
86+
default: {
87+
const [event] = createDismissEvtParams(originalValue);
9188
onChange?.(event, originalValue);
9289
break;
90+
}
9391
}
9492
} catch (error) {
9593
onError && onError(error);

src/DateTimePickerAndroid.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/**
2+
* @format
3+
* @flow strict-local
4+
*/
15
import {Platform} from 'react-native';
26

37
const warn = () => {

0 commit comments

Comments
 (0)