React-native-permissions: Split types out so module is easier to mock for testing

Created on 30 Sep 2019  路  14Comments  路  Source: zoontek/react-native-permissions

Feature Request

I'd like to see the exported types split out to a separate file, so the type-only file could be included in a mock setup file, without pulling in the logic (which checks for native module availability, and thus errors in jest test environments)

Why it is needed

Without a separate type file you have to duplicate some of the typings just to mock things effectively, as including the main logic file causes an error related to the native module being unavailable

Possible implementation

The netinfo module has a good example setup for this - it's mostly a file structure change:

https://github.com/react-native-community/react-native-netinfo/blob/master/src/internal/types.ts

Code sample

Here's how you would use it https://github.com/react-native-community/react-native-netinfo/blob/master/jest.setup.js#L11

Honestly, until I tried to mock out react-native-permissions I didn't understand why the types were split out on netinfo, and then it immediately made sense

enhancement

Most helpful comment

is there any sample test file for this lib?

All 14 comments

In the meantime, for anyone else mocking this out in testing, I have this effectively passing my tests now (though only the 'check' method from this library is exercised in my tests - so you will need to add mocks if needed)

It is basically a copy-paste of the full type structure, then mocks on the functions - fragile, but temporarily effective

./__mocks__/react-native-permissions.ts:

const ANDROID = Object.freeze({
  ACCEPT_HANDOVER: 'android.permission.ACCEPT_HANDOVER' as const,
  ACCESS_BACKGROUND_LOCATION: 'android.permission.ACCESS_BACKGROUND_LOCATION' as const,
  ACCESS_COARSE_LOCATION: 'android.permission.ACCESS_COARSE_LOCATION' as const,
  ACCESS_FINE_LOCATION: 'android.permission.ACCESS_FINE_LOCATION' as const,
  ACTIVITY_RECOGNITION: 'android.permission.ACTIVITY_RECOGNITION' as const,
  ADD_VOICEMAIL: 'com.android.voicemail.permission.ADD_VOICEMAIL' as const,
  ANSWER_PHONE_CALLS: 'android.permission.ANSWER_PHONE_CALLS' as const,
  BODY_SENSORS: 'android.permission.BODY_SENSORS' as const,
  CALL_PHONE: 'android.permission.CALL_PHONE' as const,
  CAMERA: 'android.permission.CAMERA' as const,
  GET_ACCOUNTS: 'android.permission.GET_ACCOUNTS' as const,
  PROCESS_OUTGOING_CALLS: 'android.permission.PROCESS_OUTGOING_CALLS' as const,
  READ_CALENDAR: 'android.permission.READ_CALENDAR' as const,
  READ_CALL_LOG: 'android.permission.READ_CALL_LOG' as const,
  READ_CONTACTS: 'android.permission.READ_CONTACTS' as const,
  READ_EXTERNAL_STORAGE: 'android.permission.READ_EXTERNAL_STORAGE' as const,
  READ_PHONE_NUMBERS: 'android.permission.READ_PHONE_NUMBERS' as const,
  READ_PHONE_STATE: 'android.permission.READ_PHONE_STATE' as const,
  READ_SMS: 'android.permission.READ_SMS' as const,
  RECEIVE_MMS: 'android.permission.RECEIVE_MMS' as const,
  RECEIVE_SMS: 'android.permission.RECEIVE_SMS' as const,
  RECEIVE_WAP_PUSH: 'android.permission.RECEIVE_WAP_PUSH' as const,
  RECORD_AUDIO: 'android.permission.RECORD_AUDIO' as const,
  SEND_SMS: 'android.permission.SEND_SMS' as const,
  USE_SIP: 'android.permission.USE_SIP' as const,
  WRITE_CALENDAR: 'android.permission.WRITE_CALENDAR' as const,
  WRITE_CALL_LOG: 'android.permission.WRITE_CALL_LOG' as const,
  WRITE_CONTACTS: 'android.permission.WRITE_CONTACTS' as const,
  WRITE_EXTERNAL_STORAGE: 'android.permission.WRITE_EXTERNAL_STORAGE' as const,
});

const IOS = Object.freeze({
  BLUETOOTH_PERIPHERAL: 'ios.permission.BLUETOOTH_PERIPHERAL' as const,
  CALENDARS: 'ios.permission.CALENDARS' as const,
  CAMERA: 'ios.permission.CAMERA' as const,
  CONTACTS: 'ios.permission.CONTACTS' as const,
  FACE_ID: 'ios.permission.FACE_ID' as const,
  LOCATION_ALWAYS: 'ios.permission.LOCATION_ALWAYS' as const,
  LOCATION_WHEN_IN_USE: 'ios.permission.LOCATION_WHEN_IN_USE' as const,
  MEDIA_LIBRARY: 'ios.permission.MEDIA_LIBRARY' as const,
  MICROPHONE: 'ios.permission.MICROPHONE' as const,
  MOTION: 'ios.permission.MOTION' as const,
  PHOTO_LIBRARY: 'ios.permission.PHOTO_LIBRARY' as const,
  REMINDERS: 'ios.permission.REMINDERS' as const,
  SIRI: 'ios.permission.SIRI' as const,
  SPEECH_RECOGNITION: 'ios.permission.SPEECH_RECOGNITION' as const,
  STOREKIT: 'ios.permission.STOREKIT' as const,
});

export const PERMISSIONS = Object.freeze({ ANDROID, IOS });

export const RESULTS = Object.freeze({
  UNAVAILABLE: 'unavailable' as const,
  DENIED: 'denied' as const,
  BLOCKED: 'blocked' as const,
  GRANTED: 'granted' as const,
});

type Values<T extends object> = T[keyof T];

export type AndroidPermission = Values<typeof ANDROID>;
export type IOSPermission = Values<typeof IOS>;
export type Permission = AndroidPermission | IOSPermission;

export type PermissionStatus = Values<typeof RESULTS>;

// mock out any functions you want in this style...
export async function check(permission: Permission) {
  jest.fn();
}

@mikehardy That's a good idea. I will add it in the next minor release + add documentation about testing / mocking.

And let me say, using 2.0 and just launched it out to my formal dev envs (a real TestFlight launch, and Android installs) and it seems to work great. The imports were obviously different (I use typescript) and the methods changed just a little but all in all easy upgrade and works great and I got to remove 5 plist permission entries on the iOS side. Thank you sir!

@mikehardy Should be OK, give it a look: https://github.com/react-native-community/react-native-permissions/tree/master/src. I will probably fix #329 before making the release.

Oh great! I'll give it a whirl in the next couple days but I'm sure it's fine, and this will be a help. Thanks @zoontek

Update - with v2.0.2 I am now using this as a mock - I no longer need the types but I still need the constants, I'd love to be able to PR a fix (or simply demonstrate how to use the currently existing typing correctly) but my type skills are too weak at the moment.

__mocks__/react-native-permissions.ts:

import * as RNPermission from 'react-native-permissions/lib/typescript';

const ANDROID = Object.freeze({
  ACCEPT_HANDOVER: 'android.permission.ACCEPT_HANDOVER' as const,
  ACCESS_BACKGROUND_LOCATION: 'android.permission.ACCESS_BACKGROUND_LOCATION' as const,
  ACCESS_COARSE_LOCATION: 'android.permission.ACCESS_COARSE_LOCATION' as const,
  ACCESS_FINE_LOCATION: 'android.permission.ACCESS_FINE_LOCATION' as const,
  ACTIVITY_RECOGNITION: 'android.permission.ACTIVITY_RECOGNITION' as const,
  ADD_VOICEMAIL: 'com.android.voicemail.permission.ADD_VOICEMAIL' as const,
  ANSWER_PHONE_CALLS: 'android.permission.ANSWER_PHONE_CALLS' as const,
  BODY_SENSORS: 'android.permission.BODY_SENSORS' as const,
  CALL_PHONE: 'android.permission.CALL_PHONE' as const,
  CAMERA: 'android.permission.CAMERA' as const,
  GET_ACCOUNTS: 'android.permission.GET_ACCOUNTS' as const,
  PROCESS_OUTGOING_CALLS: 'android.permission.PROCESS_OUTGOING_CALLS' as const,
  READ_CALENDAR: 'android.permission.READ_CALENDAR' as const,
  READ_CALL_LOG: 'android.permission.READ_CALL_LOG' as const,
  READ_CONTACTS: 'android.permission.READ_CONTACTS' as const,
  READ_EXTERNAL_STORAGE: 'android.permission.READ_EXTERNAL_STORAGE' as const,
  READ_PHONE_NUMBERS: 'android.permission.READ_PHONE_NUMBERS' as const,
  READ_PHONE_STATE: 'android.permission.READ_PHONE_STATE' as const,
  READ_SMS: 'android.permission.READ_SMS' as const,
  RECEIVE_MMS: 'android.permission.RECEIVE_MMS' as const,
  RECEIVE_SMS: 'android.permission.RECEIVE_SMS' as const,
  RECEIVE_WAP_PUSH: 'android.permission.RECEIVE_WAP_PUSH' as const,
  RECORD_AUDIO: 'android.permission.RECORD_AUDIO' as const,
  SEND_SMS: 'android.permission.SEND_SMS' as const,
  USE_SIP: 'android.permission.USE_SIP' as const,
  WRITE_CALENDAR: 'android.permission.WRITE_CALENDAR' as const,
  WRITE_CALL_LOG: 'android.permission.WRITE_CALL_LOG' as const,
  WRITE_CONTACTS: 'android.permission.WRITE_CONTACTS' as const,
  WRITE_EXTERNAL_STORAGE: 'android.permission.WRITE_EXTERNAL_STORAGE' as const,
});

const IOS = Object.freeze({
  BLUETOOTH_PERIPHERAL: 'ios.permission.BLUETOOTH_PERIPHERAL' as const,
  CALENDARS: 'ios.permission.CALENDARS' as const,
  CAMERA: 'ios.permission.CAMERA' as const,
  CONTACTS: 'ios.permission.CONTACTS' as const,
  FACE_ID: 'ios.permission.FACE_ID' as const,
  LOCATION_ALWAYS: 'ios.permission.LOCATION_ALWAYS' as const,
  LOCATION_WHEN_IN_USE: 'ios.permission.LOCATION_WHEN_IN_USE' as const,
  MEDIA_LIBRARY: 'ios.permission.MEDIA_LIBRARY' as const,
  MICROPHONE: 'ios.permission.MICROPHONE' as const,
  MOTION: 'ios.permission.MOTION' as const,
  PHOTO_LIBRARY: 'ios.permission.PHOTO_LIBRARY' as const,
  REMINDERS: 'ios.permission.REMINDERS' as const,
  SIRI: 'ios.permission.SIRI' as const,
  SPEECH_RECOGNITION: 'ios.permission.SPEECH_RECOGNITION' as const,
  STOREKIT: 'ios.permission.STOREKIT' as const,
});

export const PERMISSIONS = Object.freeze({ ANDROID, IOS });

export const RESULTS = Object.freeze({
  UNAVAILABLE: 'unavailable' as const,
  DENIED: 'denied' as const,
  BLOCKED: 'blocked' as const,
  GRANTED: 'granted' as const,
});

// mock out any functions you want in this style...
export async function check(permission: RNPermission.Permission) {
  jest.fn();
}

@mikehardy I also split constants when splitting types, thinking it will be convenient: https://github.com/react-native-community/react-native-permissions/blob/master/src/constants.ts

import { ANDROID, IOS, PERMISSIONS, RESULTS } from 'react-native-permissions/src/constants.ts';

It should do the trick. I will add a jestSetup.js file soon to automatically mock the module.

Yeah, I tried that but jest was unhappy with the second import on constants

Yeah, I tried that but jest was unhappy with the second import on constants

@mikehardy Try with

const {
  PERMISSIONS,
  RESULTS,
} = require('react-native-permissions/lib/commonjs/constants.js');

Success! @zoontek - current mock is this and looks good to go, I am not violating the DRY principle anywhere any more :-)

__mocks__/react-native-permission.ts

import * as RNPermission from 'react-native-permissions/lib/typescript';
const { PERMISSIONS, RESULTS } = require('react-native-permissions/lib/commonjs/constants.js');

export { PERMISSIONS, RESULTS };
// mock out any functions you want in this style...
export async function check(permission: RNPermission.Permission) {
  jest.fn();
}

is there any sample test file for this lib?

For anyone finding this after upgrading to version 3, the constants are now available in a different directory:

const {
  RESULTS,
} = require('../node_modules/react-native-permissions/dist/commonjs/results.js')
const {
  PERMISSIONS,
} = require('../node_modules/react-native-permissions/dist/commonjs/permissions.js')

export { PERMISSIONS, RESULTS }

export async function check() {
  jest.fn()
}

Reading the PERMISSION directly from '../node_modules/react-native-permissions/dist/commonjs/permissions.js'
results in empty objects:

const PERMISSIONS = Object.freeze({
  ANDROID: {},
  IOS: {},
  WINDOWS: {}
});

To use the actual PERMISSIONS in the tests I needed to adjust the mock to

const {
  RESULTS,
} = require('../node_modules/react-native-permissions/dist/commonjs/results.js')

const androidPermissions = require('../node_modules/react-native-permissions/dist/commonjs/permissions.android.js')
const iosPermissions = require('../node_modules/react-native-permissions/dist/commonjs/permissions.ios.js')

const PERMISSIONS = {
  ANDROID: androidPermissions.PERMISSIONS.ANDROID,
  IOS: iosPermissions.PERMISSIONS.IOS,
  WINDOWS: {}
};

export { PERMISSIONS, RESULTS }

export async function check() {
  jest.fn()
}
Was this page helpful?
0 / 5 - 0 ratings