@croacroa/react-native-template 2.1.0 → 3.2.0

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 (172) hide show
  1. package/.env.example +5 -0
  2. package/.eslintrc.js +8 -0
  3. package/.github/workflows/ci.yml +187 -187
  4. package/.github/workflows/eas-build.yml +55 -55
  5. package/.github/workflows/eas-update.yml +50 -50
  6. package/.github/workflows/npm-publish.yml +57 -0
  7. package/CHANGELOG.md +195 -106
  8. package/CONTRIBUTING.md +377 -377
  9. package/LICENSE +21 -21
  10. package/README.md +446 -402
  11. package/__tests__/accessibility/components.test.tsx +285 -0
  12. package/__tests__/components/Button.test.tsx +2 -4
  13. package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
  14. package/__tests__/components/snapshots.test.tsx +131 -131
  15. package/__tests__/helpers/a11y.ts +54 -0
  16. package/__tests__/hooks/useAnalytics.test.ts +100 -0
  17. package/__tests__/hooks/useAnimations.test.ts +70 -0
  18. package/__tests__/hooks/useAuth.test.tsx +71 -28
  19. package/__tests__/hooks/useMedia.test.ts +318 -0
  20. package/__tests__/hooks/usePayments.test.tsx +307 -0
  21. package/__tests__/hooks/usePermission.test.ts +230 -0
  22. package/__tests__/hooks/useWebSocket.test.ts +329 -0
  23. package/__tests__/integration/auth-api.test.tsx +224 -227
  24. package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
  25. package/__tests__/services/api.test.ts +24 -6
  26. package/app/(auth)/home.tsx +11 -9
  27. package/app/(auth)/profile.tsx +8 -6
  28. package/app/(auth)/settings.tsx +11 -9
  29. package/app/(public)/forgot-password.tsx +25 -15
  30. package/app/(public)/login.tsx +48 -12
  31. package/app/(public)/onboarding.tsx +5 -5
  32. package/app/(public)/register.tsx +24 -15
  33. package/app/_layout.tsx +6 -3
  34. package/app.config.ts +27 -2
  35. package/assets/images/.gitkeep +7 -7
  36. package/assets/images/adaptive-icon.png +0 -0
  37. package/assets/images/favicon.png +0 -0
  38. package/assets/images/icon.png +0 -0
  39. package/assets/images/notification-icon.png +0 -0
  40. package/assets/images/splash.png +0 -0
  41. package/components/ErrorBoundary.tsx +73 -28
  42. package/components/auth/SocialLoginButtons.tsx +168 -0
  43. package/components/forms/FormInput.tsx +5 -3
  44. package/components/onboarding/OnboardingScreen.tsx +370 -370
  45. package/components/onboarding/index.ts +2 -2
  46. package/components/providers/AnalyticsProvider.tsx +67 -0
  47. package/components/providers/SuspenseBoundary.tsx +359 -357
  48. package/components/providers/index.ts +24 -21
  49. package/components/ui/AnimatedButton.tsx +1 -9
  50. package/components/ui/AnimatedList.tsx +98 -0
  51. package/components/ui/AnimatedScreen.tsx +89 -0
  52. package/components/ui/Avatar.tsx +319 -316
  53. package/components/ui/Badge.tsx +416 -416
  54. package/components/ui/BottomSheet.tsx +307 -307
  55. package/components/ui/Button.tsx +11 -3
  56. package/components/ui/Checkbox.tsx +261 -261
  57. package/components/ui/FeatureGate.tsx +57 -0
  58. package/components/ui/ForceUpdateScreen.tsx +108 -0
  59. package/components/ui/ImagePickerButton.tsx +180 -0
  60. package/components/ui/Input.stories.tsx +2 -10
  61. package/components/ui/Input.tsx +2 -10
  62. package/components/ui/OptimizedImage.tsx +369 -369
  63. package/components/ui/Paywall.tsx +253 -0
  64. package/components/ui/PermissionGate.tsx +155 -0
  65. package/components/ui/PurchaseButton.tsx +84 -0
  66. package/components/ui/Select.tsx +240 -240
  67. package/components/ui/Skeleton.tsx +3 -1
  68. package/components/ui/Toast.tsx +427 -418
  69. package/components/ui/UploadProgress.tsx +189 -0
  70. package/components/ui/VirtualizedList.tsx +288 -285
  71. package/components/ui/index.ts +28 -30
  72. package/constants/config.ts +135 -97
  73. package/docs/adr/001-state-management.md +79 -79
  74. package/docs/adr/002-styling-approach.md +130 -130
  75. package/docs/adr/003-data-fetching.md +155 -155
  76. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  77. package/docs/adr/README.md +78 -78
  78. package/docs/guides/analytics-posthog.md +121 -0
  79. package/docs/guides/auth-supabase.md +162 -0
  80. package/docs/guides/feature-flags-launchdarkly.md +150 -0
  81. package/docs/guides/payments-revenuecat.md +169 -0
  82. package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
  83. package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
  84. package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
  85. package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
  86. package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
  87. package/eas.json +2 -1
  88. package/hooks/index.ts +70 -40
  89. package/hooks/useAnimatedEntry.ts +204 -0
  90. package/hooks/useApi.ts +5 -4
  91. package/hooks/useAuth.tsx +7 -3
  92. package/hooks/useBiometrics.ts +295 -295
  93. package/hooks/useChannel.ts +111 -0
  94. package/hooks/useDeepLinking.ts +256 -256
  95. package/hooks/useExperiment.ts +36 -0
  96. package/hooks/useFeatureFlag.ts +59 -0
  97. package/hooks/useForceUpdate.ts +91 -0
  98. package/hooks/useImagePicker.ts +281 -375
  99. package/hooks/useInAppReview.ts +64 -0
  100. package/hooks/useMFA.ts +509 -499
  101. package/hooks/useParallax.ts +142 -0
  102. package/hooks/usePerformance.ts +434 -434
  103. package/hooks/usePermission.ts +190 -0
  104. package/hooks/usePresence.ts +129 -0
  105. package/hooks/useProducts.ts +36 -0
  106. package/hooks/usePurchase.ts +103 -0
  107. package/hooks/useRateLimit.ts +70 -0
  108. package/hooks/useSubscription.ts +49 -0
  109. package/hooks/useTrackEvent.ts +52 -0
  110. package/hooks/useTrackScreen.ts +40 -0
  111. package/hooks/useUpdates.ts +358 -358
  112. package/hooks/useUpload.ts +165 -0
  113. package/hooks/useWebSocket.ts +111 -0
  114. package/i18n/index.ts +197 -194
  115. package/i18n/locales/ar.json +170 -101
  116. package/i18n/locales/de.json +170 -101
  117. package/i18n/locales/en.json +170 -101
  118. package/i18n/locales/es.json +170 -101
  119. package/i18n/locales/fr.json +170 -101
  120. package/jest.config.js +1 -1
  121. package/maestro/README.md +113 -113
  122. package/maestro/config.yaml +35 -35
  123. package/maestro/flows/login.yaml +62 -62
  124. package/maestro/flows/mfa-login.yaml +92 -92
  125. package/maestro/flows/mfa-setup.yaml +86 -86
  126. package/maestro/flows/navigation.yaml +68 -68
  127. package/maestro/flows/offline-conflict.yaml +101 -101
  128. package/maestro/flows/offline-sync.yaml +128 -128
  129. package/maestro/flows/offline.yaml +60 -60
  130. package/maestro/flows/register.yaml +94 -94
  131. package/package.json +188 -176
  132. package/scripts/generate-placeholders.js +38 -0
  133. package/services/analytics/adapters/console.ts +50 -0
  134. package/services/analytics/analytics-adapter.ts +94 -0
  135. package/services/analytics/types.ts +73 -0
  136. package/services/analytics.ts +428 -428
  137. package/services/api.ts +419 -340
  138. package/services/auth/social/apple.ts +110 -0
  139. package/services/auth/social/google.ts +159 -0
  140. package/services/auth/social/social-auth.ts +100 -0
  141. package/services/auth/social/types.ts +80 -0
  142. package/services/authAdapter.ts +333 -333
  143. package/services/backgroundSync.ts +652 -626
  144. package/services/feature-flags/adapters/mock.ts +108 -0
  145. package/services/feature-flags/feature-flag-adapter.ts +174 -0
  146. package/services/feature-flags/types.ts +79 -0
  147. package/services/force-update.ts +140 -0
  148. package/services/index.ts +116 -54
  149. package/services/media/compression.ts +91 -0
  150. package/services/media/media-picker.ts +151 -0
  151. package/services/media/media-upload.ts +160 -0
  152. package/services/payments/adapters/mock.ts +159 -0
  153. package/services/payments/payment-adapter.ts +118 -0
  154. package/services/payments/types.ts +131 -0
  155. package/services/permissions/permission-manager.ts +284 -0
  156. package/services/permissions/types.ts +104 -0
  157. package/services/realtime/types.ts +100 -0
  158. package/services/realtime/websocket-manager.ts +441 -0
  159. package/services/security.ts +289 -286
  160. package/services/sentry.ts +4 -4
  161. package/stores/appStore.ts +9 -0
  162. package/stores/notificationStore.ts +3 -1
  163. package/tailwind.config.js +47 -47
  164. package/tsconfig.json +37 -13
  165. package/types/user.ts +1 -1
  166. package/utils/accessibility.ts +446 -446
  167. package/utils/animations/presets.ts +182 -0
  168. package/utils/animations/transitions.ts +62 -0
  169. package/utils/index.ts +63 -52
  170. package/utils/toast.ts +9 -2
  171. package/utils/validation.ts +4 -1
  172. package/utils/withAccessibility.tsx +272 -272
@@ -1,227 +1,224 @@
1
- /**
2
- * @fileoverview Integration tests for authentication and API flow
3
- * Tests the complete flow from sign-in through authenticated API calls.
4
- */
5
-
6
- import React from "react";
7
- import { render, waitFor, act } from "@testing-library/react-native";
8
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
9
- import { AuthProvider, useAuth, getAuthToken } from "@/hooks/useAuth";
10
- import { api } from "@/services/api";
11
-
12
- // Mock SecureStore
13
- const mockSecureStore: Record<string, string> = {};
14
- jest.mock("expo-secure-store", () => ({
15
- getItemAsync: jest.fn((key: string) =>
16
- Promise.resolve(mockSecureStore[key] || null)
17
- ),
18
- setItemAsync: jest.fn((key: string, value: string) => {
19
- mockSecureStore[key] = value;
20
- return Promise.resolve();
21
- }),
22
- deleteItemAsync: jest.fn((key: string) => {
23
- delete mockSecureStore[key];
24
- return Promise.resolve();
25
- }),
26
- }));
27
-
28
- // Mock expo-router
29
- jest.mock("expo-router", () => ({
30
- router: {
31
- replace: jest.fn(),
32
- push: jest.fn(),
33
- },
34
- }));
35
-
36
- // Mock toast
37
- jest.mock("@/utils/toast", () => ({
38
- toast: {
39
- success: jest.fn(),
40
- error: jest.fn(),
41
- info: jest.fn(),
42
- },
43
- handleApiError: jest.fn(),
44
- }));
45
-
46
- // Test component that uses auth
47
- function TestAuthComponent({
48
- onAuthState,
49
- }: {
50
- onAuthState: (state: {
51
- user: unknown;
52
- isAuthenticated: boolean;
53
- isLoading: boolean;
54
- }) => void;
55
- }) {
56
- const auth = useAuth();
57
-
58
- React.useEffect(() => {
59
- onAuthState({
60
- user: auth.user,
61
- isAuthenticated: auth.isAuthenticated,
62
- isLoading: auth.isLoading,
63
- });
64
- }, [auth.user, auth.isAuthenticated, auth.isLoading, onAuthState]);
65
-
66
- return null;
67
- }
68
-
69
- // Wrapper with providers
70
- function createWrapper() {
71
- const queryClient = new QueryClient({
72
- defaultOptions: {
73
- queries: {
74
- retry: false,
75
- },
76
- },
77
- });
78
-
79
- return function Wrapper({ children }: { children: React.ReactNode }) {
80
- return (
81
- <QueryClientProvider client={queryClient}>
82
- <AuthProvider>{children}</AuthProvider>
83
- </QueryClientProvider>
84
- );
85
- };
86
- }
87
-
88
- describe("Auth + API Integration", () => {
89
- beforeEach(() => {
90
- // Clear mock storage
91
- Object.keys(mockSecureStore).forEach((key) => delete mockSecureStore[key]);
92
- jest.clearAllMocks();
93
- });
94
-
95
- describe("Authentication Flow", () => {
96
- it("starts with loading state then transitions to unauthenticated", async () => {
97
- const states: Array<{ isLoading: boolean; isAuthenticated: boolean }> =
98
- [];
99
-
100
- render(
101
- <TestAuthComponent
102
- onAuthState={(state) => states.push(state)}
103
- />,
104
- { wrapper: createWrapper() }
105
- );
106
-
107
- await waitFor(() => {
108
- expect(states.length).toBeGreaterThanOrEqual(2);
109
- });
110
-
111
- // First state should be loading
112
- expect(states[0].isLoading).toBe(true);
113
-
114
- // Final state should be not loading and not authenticated
115
- const finalState = states[states.length - 1];
116
- expect(finalState.isLoading).toBe(false);
117
- expect(finalState.isAuthenticated).toBe(false);
118
- });
119
-
120
- it("restores session from stored tokens", async () => {
121
- // Pre-populate storage with valid tokens
122
- const tokens = {
123
- accessToken: "stored_access_token",
124
- refreshToken: "stored_refresh_token",
125
- expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now
126
- };
127
- const user = {
128
- id: "1",
129
- email: "stored@example.com",
130
- name: "Stored User",
131
- };
132
-
133
- mockSecureStore["auth_tokens"] = JSON.stringify(tokens);
134
- mockSecureStore["auth_user"] = JSON.stringify(user);
135
-
136
- let finalState: { user: unknown; isAuthenticated: boolean } | null = null;
137
-
138
- render(
139
- <TestAuthComponent
140
- onAuthState={(state) => {
141
- if (!state.isLoading) {
142
- finalState = state;
143
- }
144
- }}
145
- />,
146
- { wrapper: createWrapper() }
147
- );
148
-
149
- await waitFor(() => {
150
- expect(finalState).not.toBeNull();
151
- });
152
-
153
- expect(finalState?.isAuthenticated).toBe(true);
154
- expect(finalState?.user).toEqual(user);
155
- });
156
-
157
- it("handles expired tokens by refreshing", async () => {
158
- // Pre-populate storage with expired tokens
159
- const tokens = {
160
- accessToken: "expired_access_token",
161
- refreshToken: "valid_refresh_token",
162
- expiresAt: Date.now() - 1000, // Expired 1 second ago
163
- };
164
- const user = {
165
- id: "1",
166
- email: "test@example.com",
167
- name: "Test User",
168
- };
169
-
170
- mockSecureStore["auth_tokens"] = JSON.stringify(tokens);
171
- mockSecureStore["auth_user"] = JSON.stringify(user);
172
-
173
- let finalState: { isAuthenticated: boolean } | null = null;
174
-
175
- render(
176
- <TestAuthComponent
177
- onAuthState={(state) => {
178
- if (!state.isLoading) {
179
- finalState = state;
180
- }
181
- }}
182
- />,
183
- { wrapper: createWrapper() }
184
- );
185
-
186
- await waitFor(
187
- () => {
188
- expect(finalState).not.toBeNull();
189
- },
190
- { timeout: 3000 }
191
- );
192
-
193
- // Should be authenticated after token refresh (mock implementation always succeeds)
194
- expect(finalState?.isAuthenticated).toBe(true);
195
- });
196
- });
197
-
198
- describe("Token Management", () => {
199
- it("getAuthToken returns null when no tokens stored", async () => {
200
- const token = await getAuthToken();
201
- expect(token).toBeNull();
202
- });
203
-
204
- it("getAuthToken returns access token when stored", async () => {
205
- const tokens = {
206
- accessToken: "test_access_token",
207
- refreshToken: "test_refresh_token",
208
- expiresAt: Date.now() + 60 * 60 * 1000,
209
- };
210
-
211
- mockSecureStore["auth_tokens"] = JSON.stringify(tokens);
212
-
213
- const token = await getAuthToken();
214
- expect(token).toBe("test_access_token");
215
- });
216
- });
217
-
218
- describe("API Client Integration", () => {
219
- it("api client exists and has required methods", () => {
220
- expect(api).toBeDefined();
221
- expect(typeof api.get).toBe("function");
222
- expect(typeof api.post).toBe("function");
223
- expect(typeof api.patch).toBe("function");
224
- expect(typeof api.delete).toBe("function");
225
- });
226
- });
227
- });
1
+ /**
2
+ * @fileoverview Integration tests for authentication and API flow
3
+ * Tests the complete flow from sign-in through authenticated API calls.
4
+ */
5
+
6
+ import React from "react";
7
+ import { render, waitFor } from "@testing-library/react-native";
8
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
9
+ import { AuthProvider, useAuth, getAuthToken } from "@/hooks/useAuth";
10
+ import { api } from "@/services/api";
11
+
12
+ // Mock SecureStore
13
+ const mockSecureStore: Record<string, string> = {};
14
+ jest.mock("expo-secure-store", () => ({
15
+ getItemAsync: jest.fn((key: string) =>
16
+ Promise.resolve(mockSecureStore[key] || null)
17
+ ),
18
+ setItemAsync: jest.fn((key: string, value: string) => {
19
+ mockSecureStore[key] = value;
20
+ return Promise.resolve();
21
+ }),
22
+ deleteItemAsync: jest.fn((key: string) => {
23
+ delete mockSecureStore[key];
24
+ return Promise.resolve();
25
+ }),
26
+ }));
27
+
28
+ // Mock expo-router
29
+ jest.mock("expo-router", () => ({
30
+ router: {
31
+ replace: jest.fn(),
32
+ push: jest.fn(),
33
+ },
34
+ }));
35
+
36
+ // Mock toast
37
+ jest.mock("@/utils/toast", () => ({
38
+ toast: {
39
+ success: jest.fn(),
40
+ error: jest.fn(),
41
+ info: jest.fn(),
42
+ },
43
+ handleApiError: jest.fn(),
44
+ }));
45
+
46
+ // Test component that uses auth
47
+ function TestAuthComponent({
48
+ onAuthState,
49
+ }: {
50
+ onAuthState: (state: {
51
+ user: unknown;
52
+ isAuthenticated: boolean;
53
+ isLoading: boolean;
54
+ }) => void;
55
+ }) {
56
+ const auth = useAuth();
57
+
58
+ React.useEffect(() => {
59
+ onAuthState({
60
+ user: auth.user,
61
+ isAuthenticated: auth.isAuthenticated,
62
+ isLoading: auth.isLoading,
63
+ });
64
+ }, [auth.user, auth.isAuthenticated, auth.isLoading, onAuthState]);
65
+
66
+ return null;
67
+ }
68
+
69
+ // Wrapper with providers
70
+ function createWrapper() {
71
+ const queryClient = new QueryClient({
72
+ defaultOptions: {
73
+ queries: {
74
+ retry: false,
75
+ },
76
+ },
77
+ });
78
+
79
+ return function Wrapper({ children }: { children: React.ReactNode }) {
80
+ return (
81
+ <QueryClientProvider client={queryClient}>
82
+ <AuthProvider>{children}</AuthProvider>
83
+ </QueryClientProvider>
84
+ );
85
+ };
86
+ }
87
+
88
+ describe("Auth + API Integration", () => {
89
+ beforeEach(() => {
90
+ // Clear mock storage
91
+ Object.keys(mockSecureStore).forEach((key) => delete mockSecureStore[key]);
92
+ jest.clearAllMocks();
93
+ });
94
+
95
+ describe("Authentication Flow", () => {
96
+ it("starts with loading state then transitions to unauthenticated", async () => {
97
+ const states: { isLoading: boolean; isAuthenticated: boolean }[] = [];
98
+
99
+ render(
100
+ <TestAuthComponent onAuthState={(state) => states.push(state)} />,
101
+ { wrapper: createWrapper() }
102
+ );
103
+
104
+ await waitFor(() => {
105
+ expect(states.length).toBeGreaterThanOrEqual(2);
106
+ });
107
+
108
+ // First state should be loading
109
+ expect(states[0].isLoading).toBe(true);
110
+
111
+ // Final state should be not loading and not authenticated
112
+ const finalState = states[states.length - 1];
113
+ expect(finalState.isLoading).toBe(false);
114
+ expect(finalState.isAuthenticated).toBe(false);
115
+ });
116
+
117
+ it("restores session from stored tokens", async () => {
118
+ // Pre-populate storage with valid tokens
119
+ const tokens = {
120
+ accessToken: "stored_access_token",
121
+ refreshToken: "stored_refresh_token",
122
+ expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now
123
+ };
124
+ const user = {
125
+ id: "1",
126
+ email: "stored@example.com",
127
+ name: "Stored User",
128
+ };
129
+
130
+ mockSecureStore["auth_tokens"] = JSON.stringify(tokens);
131
+ mockSecureStore["auth_user"] = JSON.stringify(user);
132
+
133
+ let finalState: { user: unknown; isAuthenticated: boolean } | null = null;
134
+
135
+ render(
136
+ <TestAuthComponent
137
+ onAuthState={(state) => {
138
+ if (!state.isLoading) {
139
+ finalState = state;
140
+ }
141
+ }}
142
+ />,
143
+ { wrapper: createWrapper() }
144
+ );
145
+
146
+ await waitFor(() => {
147
+ expect(finalState).not.toBeNull();
148
+ });
149
+
150
+ expect(finalState?.isAuthenticated).toBe(true);
151
+ expect(finalState?.user).toEqual(user);
152
+ });
153
+
154
+ it("handles expired tokens by refreshing", async () => {
155
+ // Pre-populate storage with expired tokens
156
+ const tokens = {
157
+ accessToken: "expired_access_token",
158
+ refreshToken: "valid_refresh_token",
159
+ expiresAt: Date.now() - 1000, // Expired 1 second ago
160
+ };
161
+ const user = {
162
+ id: "1",
163
+ email: "test@example.com",
164
+ name: "Test User",
165
+ };
166
+
167
+ mockSecureStore["auth_tokens"] = JSON.stringify(tokens);
168
+ mockSecureStore["auth_user"] = JSON.stringify(user);
169
+
170
+ let finalState: { isAuthenticated: boolean } | null = null;
171
+
172
+ render(
173
+ <TestAuthComponent
174
+ onAuthState={(state) => {
175
+ if (!state.isLoading) {
176
+ finalState = state;
177
+ }
178
+ }}
179
+ />,
180
+ { wrapper: createWrapper() }
181
+ );
182
+
183
+ await waitFor(
184
+ () => {
185
+ expect(finalState).not.toBeNull();
186
+ },
187
+ { timeout: 3000 }
188
+ );
189
+
190
+ // Should be authenticated after token refresh (mock implementation always succeeds)
191
+ expect(finalState?.isAuthenticated).toBe(true);
192
+ });
193
+ });
194
+
195
+ describe("Token Management", () => {
196
+ it("getAuthToken returns null when no tokens stored", async () => {
197
+ const token = await getAuthToken();
198
+ expect(token).toBeNull();
199
+ });
200
+
201
+ it("getAuthToken returns access token when stored", async () => {
202
+ const tokens = {
203
+ accessToken: "test_access_token",
204
+ refreshToken: "test_refresh_token",
205
+ expiresAt: Date.now() + 60 * 60 * 1000,
206
+ };
207
+
208
+ mockSecureStore["auth_tokens"] = JSON.stringify(tokens);
209
+
210
+ const token = await getAuthToken();
211
+ expect(token).toBe("test_access_token");
212
+ });
213
+ });
214
+
215
+ describe("API Client Integration", () => {
216
+ it("api client exists and has required methods", () => {
217
+ expect(api).toBeDefined();
218
+ expect(typeof api.get).toBe("function");
219
+ expect(typeof api.post).toBe("function");
220
+ expect(typeof api.patch).toBe("function");
221
+ expect(typeof api.delete).toBe("function");
222
+ });
223
+ });
224
+ });