@ankhorage/permissions 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +27 -3
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.d.ts.map +1 -0
  5. package/dist/cli/index.js +2 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/expo/adapters/mediaLibrary.d.ts.map +1 -1
  8. package/dist/expo/adapters/mediaLibrary.js +15 -3
  9. package/dist/expo/adapters/mediaLibrary.js.map +1 -1
  10. package/dist/expo/adapters/notifications.d.ts.map +1 -1
  11. package/dist/expo/adapters/notifications.js +32 -3
  12. package/dist/expo/adapters/notifications.js.map +1 -1
  13. package/dist/expo/client.d.ts +14 -0
  14. package/dist/expo/client.d.ts.map +1 -1
  15. package/dist/expo/client.js +42 -13
  16. package/dist/expo/client.js.map +1 -1
  17. package/dist/expo/client.testSupport.d.ts +54 -0
  18. package/dist/expo/client.testSupport.d.ts.map +1 -0
  19. package/dist/expo/client.testSupport.js +183 -0
  20. package/dist/expo/client.testSupport.js.map +1 -0
  21. package/dist/expo/manifest.d.ts +5 -0
  22. package/dist/expo/manifest.d.ts.map +1 -1
  23. package/dist/expo/manifest.js +5 -0
  24. package/dist/expo/manifest.js.map +1 -1
  25. package/dist/expo/permissionResponse.d.ts.map +1 -1
  26. package/dist/expo/permissionResponse.js +16 -9
  27. package/dist/expo/permissionResponse.js.map +1 -1
  28. package/dist/react/index.d.ts.map +1 -1
  29. package/dist/react/index.js +16 -11
  30. package/dist/react/index.js.map +1 -1
  31. package/dist/state/permissionState.d.ts +2 -0
  32. package/dist/state/permissionState.d.ts.map +1 -1
  33. package/dist/state/permissionState.js +3 -1
  34. package/dist/state/permissionState.js.map +1 -1
  35. package/dist/testing/index.d.ts.map +1 -1
  36. package/dist/testing/index.js +27 -17
  37. package/dist/testing/index.js.map +1 -1
  38. package/dist/web/index.d.ts +1 -2
  39. package/dist/web/index.d.ts.map +1 -1
  40. package/dist/web/index.js +2 -76
  41. package/dist/web/index.js.map +1 -1
  42. package/dist/web/permissionStates.d.ts +10 -0
  43. package/dist/web/permissionStates.d.ts.map +1 -0
  44. package/dist/web/permissionStates.js +64 -0
  45. package/dist/web/permissionStates.js.map +1 -0
  46. package/package.json +22 -13
  47. package/src/cli/index.ts +1 -0
  48. package/src/expo/adapters/mediaLibrary.ts +28 -4
  49. package/src/expo/adapters/notifications.ts +73 -4
  50. package/src/expo/client.notifications.test.ts +106 -0
  51. package/src/expo/client.test.ts +109 -349
  52. package/src/expo/client.testSupport.ts +188 -0
  53. package/src/expo/client.ts +45 -13
  54. package/src/expo/manifest.test.ts +49 -47
  55. package/src/expo/manifest.ts +7 -5
  56. package/src/expo/modules.d.ts +18 -4
  57. package/src/expo/permissionResponse.ts +26 -9
  58. package/src/manager/createPermissionManager.test.ts +48 -50
  59. package/src/react/index.tsx +21 -13
  60. package/src/registry/permissions.test.ts +4 -3
  61. package/src/state/permissionState.test.ts +8 -1
  62. package/src/state/permissionState.ts +4 -7
  63. package/src/testing/index.ts +36 -25
  64. package/src/web/index.test.ts +149 -151
  65. package/src/web/index.ts +9 -102
  66. package/src/web/permissionStates.ts +83 -0
@@ -1,372 +1,132 @@
1
- import { beforeEach, describe, expect, mock, test } from 'bun:test';
1
+ import { beforeEach, expect, test } from 'bun:test';
2
2
 
3
3
  import { Permission, PERMISSIONS } from '../registry/permissions';
4
4
  import { createPermissionClient } from './client';
5
+ import {
6
+ audioMock,
7
+ cameraMock,
8
+ locationMock,
9
+ mediaLibraryMock,
10
+ notificationsMock,
11
+ permissionResponse,
12
+ resetExpoMocks,
13
+ } from './client.testSupport';
14
+
15
+ beforeEach(resetExpoMocks);
16
+
17
+ test('Expo client covers getStatus and request for every known permission', async () => {
18
+ const client = createPermissionClient();
19
+ for (const permission of PERMISSIONS) {
20
+ const statusState = await client.getStatus(permission);
21
+ const requestState = await client.request(permission);
22
+ expect(statusState).toMatchObject({ permission, status: 'granted', granted: true });
23
+ expect(requestState).toMatchObject({ permission, status: 'granted', granted: true });
24
+ }
25
+ expect(mediaLibraryMock.getCalls).toEqual([false, true]);
26
+ expect(mediaLibraryMock.requestCalls).toEqual([false, true]);
27
+ expect(locationMock.getCalls).toEqual(['foreground', 'background']);
28
+ expect(locationMock.requestCalls).toEqual(['foreground', 'background']);
29
+ });
5
30
 
6
- interface MockPermissionResponse {
7
- readonly status: string;
8
- readonly canAskAgain?: boolean;
9
- }
10
-
11
- type CameraMockMode = 'topLevel' | 'cameraNamespace' | 'missingExports';
12
- type AudioMockMode = 'topLevel' | 'audioModule' | 'missingExports';
13
-
14
- function permissionResponse(status: string, canAskAgain = true): MockPermissionResponse {
15
- return { status, canAskAgain };
16
- }
17
-
18
- const cameraMock = {
19
- mode: 'topLevel' as CameraMockMode,
20
- getResponse: permissionResponse('granted'),
21
- requestResponse: permissionResponse('granted'),
22
- getError: undefined as string | undefined,
23
- requestError: undefined as string | undefined,
24
- };
25
-
26
- const audioMock = {
27
- mode: 'topLevel' as AudioMockMode,
28
- getResponse: permissionResponse('granted'),
29
- requestResponse: permissionResponse('granted'),
30
- getError: undefined as string | undefined,
31
- requestError: undefined as string | undefined,
32
- };
33
-
34
- const mediaLibraryMock = {
35
- getResponse: permissionResponse('granted'),
36
- requestResponse: permissionResponse('granted'),
37
- getError: undefined as string | undefined,
38
- requestError: undefined as string | undefined,
39
- getCalls: [] as boolean[],
40
- requestCalls: [] as boolean[],
41
- };
42
-
43
- const locationMock = {
44
- foregroundGetResponse: permissionResponse('granted'),
45
- foregroundRequestResponse: permissionResponse('granted'),
46
- backgroundGetResponse: permissionResponse('granted'),
47
- backgroundRequestResponse: permissionResponse('granted'),
48
- getCalls: [] as string[],
49
- requestCalls: [] as string[],
50
- };
51
-
52
- const notificationsMock = {
53
- getResponse: permissionResponse('granted'),
54
- requestResponse: permissionResponse('granted'),
55
- getError: undefined as string | undefined,
56
- requestError: undefined as string | undefined,
57
- };
58
-
59
- void mock.module('expo-camera', () => ({
60
- get getCameraPermissionsAsync() {
61
- if (cameraMock.mode !== 'topLevel') {
62
- return undefined;
63
- }
64
-
65
- return () => {
66
- if (cameraMock.getError !== undefined) {
67
- throw new Error(cameraMock.getError);
68
- }
69
-
70
- return Promise.resolve(cameraMock.getResponse);
71
- };
72
- },
73
- get requestCameraPermissionsAsync() {
74
- if (cameraMock.mode !== 'topLevel') {
75
- return undefined;
76
- }
77
-
78
- return () => {
79
- if (cameraMock.requestError !== undefined) {
80
- throw new Error(cameraMock.requestError);
81
- }
82
-
83
- return Promise.resolve(cameraMock.requestResponse);
84
- };
85
- },
86
- get Camera() {
87
- if (cameraMock.mode !== 'cameraNamespace') {
88
- return undefined;
89
- }
90
-
91
- return {
92
- getCameraPermissionsAsync: () => Promise.resolve(cameraMock.getResponse),
93
- requestCameraPermissionsAsync: () => Promise.resolve(cameraMock.requestResponse),
94
- };
95
- },
96
- }));
97
-
98
- void mock.module('expo-audio', () => ({
99
- get getRecordingPermissionsAsync() {
100
- if (audioMock.mode !== 'topLevel') {
101
- return undefined;
102
- }
103
-
104
- return () => {
105
- if (audioMock.getError !== undefined) {
106
- throw new Error(audioMock.getError);
107
- }
108
-
109
- return Promise.resolve(audioMock.getResponse);
110
- };
111
- },
112
- get requestRecordingPermissionsAsync() {
113
- if (audioMock.mode !== 'topLevel') {
114
- return undefined;
115
- }
116
-
117
- return () => {
118
- if (audioMock.requestError !== undefined) {
119
- throw new Error(audioMock.requestError);
120
- }
121
-
122
- return Promise.resolve(audioMock.requestResponse);
123
- };
124
- },
125
- get AudioModule() {
126
- if (audioMock.mode !== 'audioModule') {
127
- return undefined;
128
- }
129
-
130
- return {
131
- getRecordingPermissionsAsync: () => Promise.resolve(audioMock.getResponse),
132
- requestRecordingPermissionsAsync: () => Promise.resolve(audioMock.requestResponse),
133
- };
134
- },
135
- }));
136
-
137
- void mock.module('expo-media-library', () => ({
138
- getPermissionsAsync: (writeOnly?: boolean) => {
139
- mediaLibraryMock.getCalls.push(writeOnly === true);
140
-
141
- if (mediaLibraryMock.getError !== undefined) {
142
- throw new Error(mediaLibraryMock.getError);
143
- }
144
-
145
- return Promise.resolve(mediaLibraryMock.getResponse);
146
- },
147
- requestPermissionsAsync: (writeOnly?: boolean) => {
148
- mediaLibraryMock.requestCalls.push(writeOnly === true);
149
-
150
- if (mediaLibraryMock.requestError !== undefined) {
151
- throw new Error(mediaLibraryMock.requestError);
152
- }
153
-
154
- return Promise.resolve(mediaLibraryMock.requestResponse);
155
- },
156
- }));
157
-
158
- void mock.module('expo-location', () => ({
159
- getForegroundPermissionsAsync: () => {
160
- locationMock.getCalls.push('foreground');
161
-
162
- return Promise.resolve(locationMock.foregroundGetResponse);
163
- },
164
- requestForegroundPermissionsAsync: () => {
165
- locationMock.requestCalls.push('foreground');
166
-
167
- return Promise.resolve(locationMock.foregroundRequestResponse);
168
- },
169
- getBackgroundPermissionsAsync: () => {
170
- locationMock.getCalls.push('background');
171
-
172
- return Promise.resolve(locationMock.backgroundGetResponse);
173
- },
174
- requestBackgroundPermissionsAsync: () => {
175
- locationMock.requestCalls.push('background');
176
-
177
- return Promise.resolve(locationMock.backgroundRequestResponse);
178
- },
179
- }));
180
-
181
- void mock.module('expo-notifications', () => ({
182
- getPermissionsAsync: () => {
183
- if (notificationsMock.getError !== undefined) {
184
- throw new Error(notificationsMock.getError);
185
- }
186
-
187
- return Promise.resolve(notificationsMock.getResponse);
188
- },
189
- requestPermissionsAsync: () => {
190
- if (notificationsMock.requestError !== undefined) {
191
- throw new Error(notificationsMock.requestError);
192
- }
193
-
194
- return Promise.resolve(notificationsMock.requestResponse);
195
- },
196
- }));
197
-
198
- describe('createPermissionClient', () => {
199
- beforeEach(() => {
200
- resetExpoMocks();
31
+ test('Expo client treats clipboard as not requiring a native module', async () => {
32
+ const client = createPermissionClient();
33
+ expect(await client.getStatus(Permission.Clipboard)).toMatchObject({
34
+ permission: Permission.Clipboard,
35
+ status: 'granted',
36
+ granted: true,
201
37
  });
202
-
203
- test('covers getStatus and request for every known permission', async () => {
204
- const client = createPermissionClient();
205
-
206
- for (const permission of PERMISSIONS) {
207
- const statusState = await client.getStatus(permission);
208
- const requestState = await client.request(permission);
209
-
210
- expect(statusState.permission).toBe(permission);
211
- expect(statusState.status).toBe('granted');
212
- expect(statusState.granted).toBe(true);
213
-
214
- expect(requestState.permission).toBe(permission);
215
- expect(requestState.status).toBe('granted');
216
- expect(requestState.granted).toBe(true);
217
- }
218
-
219
- expect(mediaLibraryMock.getCalls).toEqual([false, true]);
220
- expect(mediaLibraryMock.requestCalls).toEqual([false, true]);
221
- expect(locationMock.getCalls).toEqual(['foreground', 'background']);
222
- expect(locationMock.requestCalls).toEqual(['foreground', 'background']);
38
+ expect(await client.request(Permission.Clipboard)).toMatchObject({
39
+ permission: Permission.Clipboard,
40
+ status: 'granted',
41
+ granted: true,
223
42
  });
43
+ });
224
44
 
225
- test('treats clipboard as not requiring a native Expo permission module', async () => {
226
- const client = createPermissionClient();
227
-
228
- const statusState = await client.getStatus(Permission.Clipboard);
229
- const requestState = await client.request(Permission.Clipboard);
230
-
231
- expect(statusState).toMatchObject({
232
- permission: Permission.Clipboard,
233
- status: 'granted',
234
- granted: true,
235
- });
236
- expect(requestState).toMatchObject({
237
- permission: Permission.Clipboard,
238
- status: 'granted',
239
- granted: true,
240
- });
45
+ test('Expo client falls back to the Camera namespace', async () => {
46
+ cameraMock.mode = 'cameraNamespace';
47
+ cameraMock.getResponse = permissionResponse('denied', false);
48
+ cameraMock.requestResponse = permissionResponse('granted');
49
+ const client = createPermissionClient();
50
+ expect(await client.getStatus(Permission.Camera)).toMatchObject({
51
+ permission: Permission.Camera,
52
+ status: 'blocked',
53
+ granted: false,
54
+ canAskAgain: false,
55
+ reason: 'Permission is blocked. Open system settings to change it.',
241
56
  });
242
-
243
- test('falls back to the Camera namespace when camera permission functions are nested', async () => {
244
- cameraMock.mode = 'cameraNamespace';
245
- cameraMock.getResponse = permissionResponse('denied', false);
246
- cameraMock.requestResponse = permissionResponse('granted');
247
-
248
- const client = createPermissionClient();
249
-
250
- expect(await client.getStatus(Permission.Camera)).toMatchObject({
251
- permission: Permission.Camera,
252
- status: 'denied',
253
- granted: false,
254
- canAskAgain: false,
255
- });
256
- expect(await client.request(Permission.Camera)).toMatchObject({
257
- permission: Permission.Camera,
258
- status: 'granted',
259
- granted: true,
260
- });
57
+ expect(await client.request(Permission.Camera)).toMatchObject({
58
+ permission: Permission.Camera,
59
+ status: 'granted',
60
+ granted: true,
261
61
  });
62
+ });
262
63
 
263
- test('falls back to AudioModule recording permission functions when top-level exports are absent', async () => {
264
- audioMock.mode = 'audioModule';
265
- audioMock.getResponse = permissionResponse('denied', false);
266
- audioMock.requestResponse = permissionResponse('granted');
267
-
268
- const client = createPermissionClient();
269
-
270
- expect(await client.getStatus(Permission.Microphone)).toMatchObject({
271
- permission: Permission.Microphone,
272
- status: 'denied',
273
- granted: false,
274
- canAskAgain: false,
275
- });
276
- expect(await client.request(Permission.Microphone)).toMatchObject({
277
- permission: Permission.Microphone,
278
- status: 'granted',
279
- granted: true,
280
- });
64
+ test('Expo client falls back to AudioModule recording permissions', async () => {
65
+ audioMock.mode = 'audioModule';
66
+ audioMock.getResponse = permissionResponse('denied', false);
67
+ audioMock.requestResponse = permissionResponse('granted');
68
+ const client = createPermissionClient();
69
+ expect(await client.getStatus(Permission.Microphone)).toMatchObject({
70
+ permission: Permission.Microphone,
71
+ status: 'blocked',
72
+ granted: false,
73
+ canAskAgain: false,
74
+ reason: 'Permission is blocked. Open system settings to change it.',
281
75
  });
282
-
283
- test('normalizes unknown adapter statuses to unknown', async () => {
284
- notificationsMock.getResponse = permissionResponse('mystery-status', false);
285
- notificationsMock.requestResponse = permissionResponse('denied', false);
286
-
287
- const client = createPermissionClient();
288
- const state = await client.getStatus(Permission.Notifications);
289
-
290
- expect(state).toMatchObject({
291
- permission: Permission.Notifications,
292
- status: 'unknown',
293
- granted: false,
294
- canAskAgain: false,
295
- });
76
+ expect(await client.request(Permission.Microphone)).toMatchObject({
77
+ permission: Permission.Microphone,
78
+ status: 'granted',
79
+ granted: true,
296
80
  });
81
+ });
297
82
 
298
- test('preserves denied request responses', async () => {
299
- notificationsMock.requestResponse = permissionResponse('denied', false);
300
-
301
- const client = createPermissionClient();
302
- const state = await client.request(Permission.Notifications);
83
+ test('Expo client normalizes unknown adapter statuses', async () => {
84
+ notificationsMock.getResponse = permissionResponse('mystery-status', false);
85
+ const state = await createPermissionClient().getStatus(Permission.Notifications);
86
+ expect(state).toMatchObject({
87
+ permission: Permission.Notifications,
88
+ status: 'unknown',
89
+ granted: false,
90
+ canAskAgain: false,
91
+ });
92
+ });
303
93
 
94
+ test('Expo client normalizes non-requestable denials to blocked', async () => {
95
+ notificationsMock.getResponse = permissionResponse('denied', false);
96
+ notificationsMock.requestResponse = permissionResponse('denied', false);
97
+ const client = createPermissionClient();
98
+ for (const state of [
99
+ await client.getStatus(Permission.Notifications),
100
+ await client.request(Permission.Notifications),
101
+ ]) {
304
102
  expect(state).toMatchObject({
305
103
  permission: Permission.Notifications,
306
- status: 'denied',
104
+ status: 'blocked',
307
105
  granted: false,
308
106
  canAskAgain: false,
107
+ reason: 'Permission is blocked. Open system settings to change it.',
309
108
  });
310
- });
311
-
312
- test('returns unavailable when a camera permission call fails at runtime', async () => {
313
- cameraMock.getError = 'camera bridge unavailable';
314
-
315
- const client = createPermissionClient();
316
- const state = await client.getStatus(Permission.Camera);
317
-
318
- expect(state).toMatchObject({
319
- permission: Permission.Camera,
320
- status: 'unavailable',
321
- granted: false,
322
- reason: 'camera bridge unavailable',
323
- });
324
- });
325
-
326
- test('returns unavailable when a request call throws at runtime', async () => {
327
- audioMock.requestError = 'microphone bridge unavailable';
328
-
329
- const client = createPermissionClient();
330
- const state = await client.request(Permission.Microphone);
109
+ }
110
+ });
331
111
 
112
+ test('Expo client normalizes limited media-library access', async () => {
113
+ mediaLibraryMock.getResponse = {
114
+ status: 'granted',
115
+ canAskAgain: false,
116
+ accessPrivileges: 'limited',
117
+ };
118
+ mediaLibraryMock.requestResponse = mediaLibraryMock.getResponse;
119
+ const client = createPermissionClient();
120
+ for (const state of [
121
+ await client.getStatus(Permission.MediaLibrary),
122
+ await client.request(Permission.MediaLibrary),
123
+ ]) {
332
124
  expect(state).toMatchObject({
333
- permission: Permission.Microphone,
334
- status: 'unavailable',
335
- granted: false,
336
- reason: 'microphone bridge unavailable',
125
+ permission: Permission.MediaLibrary,
126
+ status: 'limited',
127
+ granted: true,
128
+ canAskAgain: false,
129
+ reason: 'Media library access is limited to user-selected assets.',
337
130
  });
338
- });
131
+ }
339
132
  });
340
-
341
- function resetExpoMocks(): void {
342
- cameraMock.mode = 'topLevel';
343
- cameraMock.getResponse = permissionResponse('granted');
344
- cameraMock.requestResponse = permissionResponse('granted');
345
- cameraMock.getError = undefined;
346
- cameraMock.requestError = undefined;
347
-
348
- audioMock.mode = 'topLevel';
349
- audioMock.getResponse = permissionResponse('granted');
350
- audioMock.requestResponse = permissionResponse('granted');
351
- audioMock.getError = undefined;
352
- audioMock.requestError = undefined;
353
-
354
- mediaLibraryMock.getResponse = permissionResponse('granted');
355
- mediaLibraryMock.requestResponse = permissionResponse('granted');
356
- mediaLibraryMock.getError = undefined;
357
- mediaLibraryMock.requestError = undefined;
358
- mediaLibraryMock.getCalls.length = 0;
359
- mediaLibraryMock.requestCalls.length = 0;
360
-
361
- locationMock.foregroundGetResponse = permissionResponse('granted');
362
- locationMock.foregroundRequestResponse = permissionResponse('granted');
363
- locationMock.backgroundGetResponse = permissionResponse('granted');
364
- locationMock.backgroundRequestResponse = permissionResponse('granted');
365
- locationMock.getCalls.length = 0;
366
- locationMock.requestCalls.length = 0;
367
-
368
- notificationsMock.getResponse = permissionResponse('granted');
369
- notificationsMock.requestResponse = permissionResponse('granted');
370
- notificationsMock.getError = undefined;
371
- notificationsMock.requestError = undefined;
372
- }
@@ -0,0 +1,188 @@
1
+ import { mock } from 'bun:test';
2
+
3
+ export interface MockPermissionResponse {
4
+ readonly status: string;
5
+ readonly canAskAgain?: boolean;
6
+ readonly accessPrivileges?: 'all' | 'limited' | 'none';
7
+ readonly ios?: { readonly status?: number };
8
+ }
9
+
10
+ type CameraMockMode = 'topLevel' | 'cameraNamespace' | 'missingExports';
11
+ type AudioMockMode = 'topLevel' | 'audioModule' | 'missingExports';
12
+
13
+ export function permissionResponse(status: string, canAskAgain = true): MockPermissionResponse {
14
+ return { status, canAskAgain };
15
+ }
16
+
17
+ export const cameraMock = {
18
+ mode: 'topLevel' as CameraMockMode,
19
+ getResponse: permissionResponse('granted'),
20
+ requestResponse: permissionResponse('granted'),
21
+ getError: undefined as string | undefined,
22
+ requestError: undefined as string | undefined,
23
+ };
24
+ export const audioMock = {
25
+ mode: 'topLevel' as AudioMockMode,
26
+ getResponse: permissionResponse('granted'),
27
+ requestResponse: permissionResponse('granted'),
28
+ getError: undefined as string | undefined,
29
+ requestError: undefined as string | undefined,
30
+ };
31
+ export const mediaLibraryMock = {
32
+ getResponse: permissionResponse('granted'),
33
+ requestResponse: permissionResponse('granted'),
34
+ getError: undefined as string | undefined,
35
+ requestError: undefined as string | undefined,
36
+ getCalls: [] as boolean[],
37
+ requestCalls: [] as boolean[],
38
+ };
39
+ export const locationMock = {
40
+ foregroundGetResponse: permissionResponse('granted'),
41
+ foregroundRequestResponse: permissionResponse('granted'),
42
+ backgroundGetResponse: permissionResponse('granted'),
43
+ backgroundRequestResponse: permissionResponse('granted'),
44
+ getCalls: [] as string[],
45
+ requestCalls: [] as string[],
46
+ };
47
+ export const notificationsMock = {
48
+ getResponse: permissionResponse('granted'),
49
+ requestResponse: permissionResponse('granted'),
50
+ getError: undefined as string | undefined,
51
+ requestError: undefined as string | undefined,
52
+ };
53
+ export const linkingMock = { calls: 0, error: undefined as Error | undefined };
54
+
55
+ void mock.module('expo-camera', () => ({
56
+ get getCameraPermissionsAsync() {
57
+ if (cameraMock.mode !== 'topLevel') return undefined;
58
+ return () => {
59
+ if (cameraMock.getError !== undefined) throw new Error(cameraMock.getError);
60
+ return Promise.resolve(cameraMock.getResponse);
61
+ };
62
+ },
63
+ get requestCameraPermissionsAsync() {
64
+ if (cameraMock.mode !== 'topLevel') return undefined;
65
+ return () => {
66
+ if (cameraMock.requestError !== undefined) throw new Error(cameraMock.requestError);
67
+ return Promise.resolve(cameraMock.requestResponse);
68
+ };
69
+ },
70
+ get Camera() {
71
+ if (cameraMock.mode !== 'cameraNamespace') return undefined;
72
+ return {
73
+ getCameraPermissionsAsync: () => Promise.resolve(cameraMock.getResponse),
74
+ requestCameraPermissionsAsync: () => Promise.resolve(cameraMock.requestResponse),
75
+ };
76
+ },
77
+ }));
78
+
79
+ void mock.module('expo-audio', () => ({
80
+ get getRecordingPermissionsAsync() {
81
+ if (audioMock.mode !== 'topLevel') return undefined;
82
+ return () => {
83
+ if (audioMock.getError !== undefined) throw new Error(audioMock.getError);
84
+ return Promise.resolve(audioMock.getResponse);
85
+ };
86
+ },
87
+ get requestRecordingPermissionsAsync() {
88
+ if (audioMock.mode !== 'topLevel') return undefined;
89
+ return () => {
90
+ if (audioMock.requestError !== undefined) throw new Error(audioMock.requestError);
91
+ return Promise.resolve(audioMock.requestResponse);
92
+ };
93
+ },
94
+ get AudioModule() {
95
+ if (audioMock.mode !== 'audioModule') return undefined;
96
+ return {
97
+ getRecordingPermissionsAsync: () => Promise.resolve(audioMock.getResponse),
98
+ requestRecordingPermissionsAsync: () => Promise.resolve(audioMock.requestResponse),
99
+ };
100
+ },
101
+ }));
102
+
103
+ void mock.module('expo-media-library', () => ({
104
+ getPermissionsAsync: (writeOnly?: boolean) => {
105
+ mediaLibraryMock.getCalls.push(writeOnly === true);
106
+ if (mediaLibraryMock.getError !== undefined) throw new Error(mediaLibraryMock.getError);
107
+ return Promise.resolve(mediaLibraryMock.getResponse);
108
+ },
109
+ requestPermissionsAsync: (writeOnly?: boolean) => {
110
+ mediaLibraryMock.requestCalls.push(writeOnly === true);
111
+ if (mediaLibraryMock.requestError !== undefined) throw new Error(mediaLibraryMock.requestError);
112
+ return Promise.resolve(mediaLibraryMock.requestResponse);
113
+ },
114
+ }));
115
+
116
+ void mock.module('expo-location', () => ({
117
+ getForegroundPermissionsAsync: () => {
118
+ locationMock.getCalls.push('foreground');
119
+ return Promise.resolve(locationMock.foregroundGetResponse);
120
+ },
121
+ requestForegroundPermissionsAsync: () => {
122
+ locationMock.requestCalls.push('foreground');
123
+ return Promise.resolve(locationMock.foregroundRequestResponse);
124
+ },
125
+ getBackgroundPermissionsAsync: () => {
126
+ locationMock.getCalls.push('background');
127
+ return Promise.resolve(locationMock.backgroundGetResponse);
128
+ },
129
+ requestBackgroundPermissionsAsync: () => {
130
+ locationMock.requestCalls.push('background');
131
+ return Promise.resolve(locationMock.backgroundRequestResponse);
132
+ },
133
+ }));
134
+
135
+ void mock.module('expo-notifications', () => ({
136
+ getPermissionsAsync: () => {
137
+ if (notificationsMock.getError !== undefined) throw new Error(notificationsMock.getError);
138
+ return Promise.resolve(notificationsMock.getResponse);
139
+ },
140
+ requestPermissionsAsync: () => {
141
+ if (notificationsMock.requestError !== undefined)
142
+ throw new Error(notificationsMock.requestError);
143
+ return Promise.resolve(notificationsMock.requestResponse);
144
+ },
145
+ }));
146
+
147
+ void mock.module('expo-linking', () => ({
148
+ openSettings: () => {
149
+ linkingMock.calls += 1;
150
+ if (linkingMock.error !== undefined) throw linkingMock.error;
151
+ return Promise.resolve();
152
+ },
153
+ }));
154
+
155
+ export function resetExpoMocks(): void {
156
+ cameraMock.mode = 'topLevel';
157
+ cameraMock.getResponse = permissionResponse('granted');
158
+ cameraMock.requestResponse = permissionResponse('granted');
159
+ cameraMock.getError = undefined;
160
+ cameraMock.requestError = undefined;
161
+ audioMock.mode = 'topLevel';
162
+ audioMock.getResponse = permissionResponse('granted');
163
+ audioMock.requestResponse = permissionResponse('granted');
164
+ audioMock.getError = undefined;
165
+ audioMock.requestError = undefined;
166
+ resetCollectionMocks();
167
+ notificationsMock.getResponse = permissionResponse('granted');
168
+ notificationsMock.requestResponse = permissionResponse('granted');
169
+ notificationsMock.getError = undefined;
170
+ notificationsMock.requestError = undefined;
171
+ linkingMock.calls = 0;
172
+ linkingMock.error = undefined;
173
+ }
174
+
175
+ function resetCollectionMocks(): void {
176
+ mediaLibraryMock.getResponse = permissionResponse('granted');
177
+ mediaLibraryMock.requestResponse = permissionResponse('granted');
178
+ mediaLibraryMock.getError = undefined;
179
+ mediaLibraryMock.requestError = undefined;
180
+ mediaLibraryMock.getCalls.length = 0;
181
+ mediaLibraryMock.requestCalls.length = 0;
182
+ locationMock.foregroundGetResponse = permissionResponse('granted');
183
+ locationMock.foregroundRequestResponse = permissionResponse('granted');
184
+ locationMock.backgroundGetResponse = permissionResponse('granted');
185
+ locationMock.backgroundRequestResponse = permissionResponse('granted');
186
+ locationMock.getCalls.length = 0;
187
+ locationMock.requestCalls.length = 0;
188
+ }