@croacroa/react-native-template 2.0.1 → 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.
- package/.env.example +5 -0
- package/.eslintrc.js +8 -0
- package/.github/workflows/ci.yml +187 -187
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/.github/workflows/npm-publish.yml +57 -0
- package/CHANGELOG.md +195 -106
- package/CONTRIBUTING.md +377 -377
- package/LICENSE +21 -0
- package/README.md +446 -399
- package/__tests__/accessibility/components.test.tsx +285 -0
- package/__tests__/components/Button.test.tsx +2 -4
- package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
- package/__tests__/components/snapshots.test.tsx +131 -131
- package/__tests__/helpers/a11y.ts +54 -0
- package/__tests__/hooks/useAnalytics.test.ts +100 -0
- package/__tests__/hooks/useAnimations.test.ts +70 -0
- package/__tests__/hooks/useAuth.test.tsx +71 -28
- package/__tests__/hooks/useMedia.test.ts +318 -0
- package/__tests__/hooks/usePayments.test.tsx +307 -0
- package/__tests__/hooks/usePermission.test.ts +230 -0
- package/__tests__/hooks/useWebSocket.test.ts +329 -0
- package/__tests__/integration/auth-api.test.tsx +224 -227
- package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
- package/__tests__/services/api.test.ts +24 -6
- package/app/(auth)/home.tsx +11 -9
- package/app/(auth)/profile.tsx +8 -6
- package/app/(auth)/settings.tsx +11 -9
- package/app/(public)/forgot-password.tsx +25 -15
- package/app/(public)/login.tsx +48 -12
- package/app/(public)/onboarding.tsx +5 -5
- package/app/(public)/register.tsx +24 -15
- package/app/_layout.tsx +6 -3
- package/app.config.ts +27 -2
- package/assets/images/.gitkeep +7 -7
- package/assets/images/adaptive-icon.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/notification-icon.png +0 -0
- package/assets/images/splash.png +0 -0
- package/components/ErrorBoundary.tsx +73 -28
- package/components/auth/SocialLoginButtons.tsx +168 -0
- package/components/forms/FormInput.tsx +5 -3
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/AnalyticsProvider.tsx +67 -0
- package/components/providers/SuspenseBoundary.tsx +359 -357
- package/components/providers/index.ts +24 -21
- package/components/ui/AnimatedButton.tsx +1 -9
- package/components/ui/AnimatedList.tsx +98 -0
- package/components/ui/AnimatedScreen.tsx +89 -0
- package/components/ui/Avatar.tsx +319 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Button.tsx +11 -3
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/FeatureGate.tsx +57 -0
- package/components/ui/ForceUpdateScreen.tsx +108 -0
- package/components/ui/ImagePickerButton.tsx +180 -0
- package/components/ui/Input.stories.tsx +2 -10
- package/components/ui/Input.tsx +2 -10
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Paywall.tsx +253 -0
- package/components/ui/PermissionGate.tsx +155 -0
- package/components/ui/PurchaseButton.tsx +84 -0
- package/components/ui/Select.tsx +240 -240
- package/components/ui/Skeleton.tsx +3 -1
- package/components/ui/Toast.tsx +427 -0
- package/components/ui/UploadProgress.tsx +189 -0
- package/components/ui/VirtualizedList.tsx +288 -285
- package/components/ui/index.ts +28 -23
- package/constants/config.ts +135 -97
- package/docs/adr/001-state-management.md +79 -79
- package/docs/adr/002-styling-approach.md +130 -130
- package/docs/adr/003-data-fetching.md +155 -155
- package/docs/adr/004-auth-adapter-pattern.md +144 -144
- package/docs/adr/README.md +78 -78
- package/docs/guides/analytics-posthog.md +121 -0
- package/docs/guides/auth-supabase.md +162 -0
- package/docs/guides/feature-flags-launchdarkly.md +150 -0
- package/docs/guides/payments-revenuecat.md +169 -0
- package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
- package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
- package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
- package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
- package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
- package/eas.json +2 -1
- package/hooks/index.ts +70 -27
- package/hooks/useAnimatedEntry.ts +204 -0
- package/hooks/useApi.ts +64 -4
- package/hooks/useAuth.tsx +7 -3
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useChannel.ts +111 -0
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useExperiment.ts +36 -0
- package/hooks/useFeatureFlag.ts +59 -0
- package/hooks/useForceUpdate.ts +91 -0
- package/hooks/useImagePicker.ts +281 -0
- package/hooks/useInAppReview.ts +64 -0
- package/hooks/useMFA.ts +509 -499
- package/hooks/useParallax.ts +142 -0
- package/hooks/usePerformance.ts +434 -434
- package/hooks/usePermission.ts +190 -0
- package/hooks/usePresence.ts +129 -0
- package/hooks/useProducts.ts +36 -0
- package/hooks/usePurchase.ts +103 -0
- package/hooks/useRateLimit.ts +70 -0
- package/hooks/useSubscription.ts +49 -0
- package/hooks/useTrackEvent.ts +52 -0
- package/hooks/useTrackScreen.ts +40 -0
- package/hooks/useUpdates.ts +358 -358
- package/hooks/useUpload.ts +165 -0
- package/hooks/useWebSocket.ts +111 -0
- package/i18n/index.ts +197 -194
- package/i18n/locales/ar.json +170 -101
- package/i18n/locales/de.json +170 -101
- package/i18n/locales/en.json +170 -101
- package/i18n/locales/es.json +170 -101
- package/i18n/locales/fr.json +170 -101
- package/jest.config.js +1 -1
- package/maestro/README.md +113 -113
- package/maestro/config.yaml +35 -35
- package/maestro/flows/login.yaml +62 -62
- package/maestro/flows/mfa-login.yaml +92 -92
- package/maestro/flows/mfa-setup.yaml +86 -86
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -101
- package/maestro/flows/offline-sync.yaml +128 -128
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +188 -175
- package/scripts/generate-placeholders.js +38 -0
- package/services/analytics/adapters/console.ts +50 -0
- package/services/analytics/analytics-adapter.ts +94 -0
- package/services/analytics/types.ts +73 -0
- package/services/analytics.ts +428 -428
- package/services/api.ts +419 -340
- package/services/auth/social/apple.ts +110 -0
- package/services/auth/social/google.ts +159 -0
- package/services/auth/social/social-auth.ts +100 -0
- package/services/auth/social/types.ts +80 -0
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +652 -626
- package/services/feature-flags/adapters/mock.ts +108 -0
- package/services/feature-flags/feature-flag-adapter.ts +174 -0
- package/services/feature-flags/types.ts +79 -0
- package/services/force-update.ts +140 -0
- package/services/index.ts +116 -54
- package/services/media/compression.ts +91 -0
- package/services/media/media-picker.ts +151 -0
- package/services/media/media-upload.ts +160 -0
- package/services/payments/adapters/mock.ts +159 -0
- package/services/payments/payment-adapter.ts +118 -0
- package/services/payments/types.ts +131 -0
- package/services/permissions/permission-manager.ts +284 -0
- package/services/permissions/types.ts +104 -0
- package/services/realtime/types.ts +100 -0
- package/services/realtime/websocket-manager.ts +441 -0
- package/services/security.ts +289 -286
- package/services/sentry.ts +4 -4
- package/stores/appStore.ts +9 -0
- package/stores/notificationStore.ts +3 -1
- package/tailwind.config.js +47 -47
- package/tsconfig.json +37 -13
- package/types/user.ts +1 -1
- package/utils/accessibility.ts +446 -446
- package/utils/animations/presets.ts +182 -0
- package/utils/animations/transitions.ts +62 -0
- package/utils/index.ts +63 -52
- package/utils/toast.ts +9 -2
- package/utils/validation.ts +4 -1
- package/utils/withAccessibility.tsx +272 -272
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Snapshot tests for UI components
|
|
3
|
-
* Ensures visual consistency of components across changes.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React from "react";
|
|
7
|
-
import { render } from "@testing-library/react-native";
|
|
8
|
-
import { Text, View } from "react-native";
|
|
9
|
-
|
|
10
|
-
import { Button } from "@/components/ui/Button";
|
|
11
|
-
import { Card } from "@/components/ui/Card";
|
|
12
|
-
|
|
13
|
-
// Mock useTheme hook for components that use it
|
|
14
|
-
jest.mock("@/hooks/useTheme", () => ({
|
|
15
|
-
useTheme: () => ({
|
|
16
|
-
isDark: false,
|
|
17
|
-
mode: "light",
|
|
18
|
-
isLoaded: true,
|
|
19
|
-
setMode: jest.fn(),
|
|
20
|
-
toggleTheme: jest.fn(),
|
|
21
|
-
}),
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
describe("UI Components Snapshots", () => {
|
|
25
|
-
describe("Button", () => {
|
|
26
|
-
it("renders primary variant correctly", () => {
|
|
27
|
-
const { toJSON } = render(<Button variant="primary">Primary</Button>);
|
|
28
|
-
expect(toJSON()).toMatchSnapshot();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("renders secondary variant correctly", () => {
|
|
32
|
-
const { toJSON } = render(<Button variant="secondary">Secondary</Button>);
|
|
33
|
-
expect(toJSON()).toMatchSnapshot();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("renders outline variant correctly", () => {
|
|
37
|
-
const { toJSON } = render(<Button variant="outline">Outline</Button>);
|
|
38
|
-
expect(toJSON()).toMatchSnapshot();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("renders ghost variant correctly", () => {
|
|
42
|
-
const { toJSON } = render(<Button variant="ghost">Ghost</Button>);
|
|
43
|
-
expect(toJSON()).toMatchSnapshot();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("renders danger variant correctly", () => {
|
|
47
|
-
const { toJSON } = render(<Button variant="danger">Danger</Button>);
|
|
48
|
-
expect(toJSON()).toMatchSnapshot();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("renders small size correctly", () => {
|
|
52
|
-
const { toJSON } = render(<Button size="sm">Small</Button>);
|
|
53
|
-
expect(toJSON()).toMatchSnapshot();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("renders large size correctly", () => {
|
|
57
|
-
const { toJSON } = render(<Button size="lg">Large</Button>);
|
|
58
|
-
expect(toJSON()).toMatchSnapshot();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("renders loading state correctly", () => {
|
|
62
|
-
const { toJSON } = render(<Button isLoading>Loading</Button>);
|
|
63
|
-
expect(toJSON()).toMatchSnapshot();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("renders disabled state correctly", () => {
|
|
67
|
-
const { toJSON } = render(<Button disabled>Disabled</Button>);
|
|
68
|
-
expect(toJSON()).toMatchSnapshot();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("renders with custom children correctly", () => {
|
|
72
|
-
const { toJSON } = render(
|
|
73
|
-
<Button>
|
|
74
|
-
<View>
|
|
75
|
-
<Text>Custom Content</Text>
|
|
76
|
-
</View>
|
|
77
|
-
</Button>
|
|
78
|
-
);
|
|
79
|
-
expect(toJSON()).toMatchSnapshot();
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe("Card", () => {
|
|
84
|
-
it("renders default variant correctly", () => {
|
|
85
|
-
const { toJSON } = render(
|
|
86
|
-
<Card>
|
|
87
|
-
<Text>Default Card</Text>
|
|
88
|
-
</Card>
|
|
89
|
-
);
|
|
90
|
-
expect(toJSON()).toMatchSnapshot();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("renders elevated variant correctly", () => {
|
|
94
|
-
const { toJSON } = render(
|
|
95
|
-
<Card variant="elevated">
|
|
96
|
-
<Text>Elevated Card</Text>
|
|
97
|
-
</Card>
|
|
98
|
-
);
|
|
99
|
-
expect(toJSON()).toMatchSnapshot();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("renders outlined variant correctly", () => {
|
|
103
|
-
const { toJSON } = render(
|
|
104
|
-
<Card variant="outlined">
|
|
105
|
-
<Text>Outlined Card</Text>
|
|
106
|
-
</Card>
|
|
107
|
-
);
|
|
108
|
-
expect(toJSON()).toMatchSnapshot();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("renders with custom className correctly", () => {
|
|
112
|
-
const { toJSON } = render(
|
|
113
|
-
<Card className="p-4 m-2">
|
|
114
|
-
<Text>Custom Styled Card</Text>
|
|
115
|
-
</Card>
|
|
116
|
-
);
|
|
117
|
-
expect(toJSON()).toMatchSnapshot();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("renders complex content correctly", () => {
|
|
121
|
-
const { toJSON } = render(
|
|
122
|
-
<Card variant="elevated" className="p-4">
|
|
123
|
-
<Text>Title</Text>
|
|
124
|
-
<Text>Description text here</Text>
|
|
125
|
-
<Button>Action</Button>
|
|
126
|
-
</Card>
|
|
127
|
-
);
|
|
128
|
-
expect(toJSON()).toMatchSnapshot();
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Snapshot tests for UI components
|
|
3
|
+
* Ensures visual consistency of components across changes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { render } from "@testing-library/react-native";
|
|
8
|
+
import { Text, View } from "react-native";
|
|
9
|
+
|
|
10
|
+
import { Button } from "@/components/ui/Button";
|
|
11
|
+
import { Card } from "@/components/ui/Card";
|
|
12
|
+
|
|
13
|
+
// Mock useTheme hook for components that use it
|
|
14
|
+
jest.mock("@/hooks/useTheme", () => ({
|
|
15
|
+
useTheme: () => ({
|
|
16
|
+
isDark: false,
|
|
17
|
+
mode: "light",
|
|
18
|
+
isLoaded: true,
|
|
19
|
+
setMode: jest.fn(),
|
|
20
|
+
toggleTheme: jest.fn(),
|
|
21
|
+
}),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe("UI Components Snapshots", () => {
|
|
25
|
+
describe("Button", () => {
|
|
26
|
+
it("renders primary variant correctly", () => {
|
|
27
|
+
const { toJSON } = render(<Button variant="primary">Primary</Button>);
|
|
28
|
+
expect(toJSON()).toMatchSnapshot();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders secondary variant correctly", () => {
|
|
32
|
+
const { toJSON } = render(<Button variant="secondary">Secondary</Button>);
|
|
33
|
+
expect(toJSON()).toMatchSnapshot();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("renders outline variant correctly", () => {
|
|
37
|
+
const { toJSON } = render(<Button variant="outline">Outline</Button>);
|
|
38
|
+
expect(toJSON()).toMatchSnapshot();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders ghost variant correctly", () => {
|
|
42
|
+
const { toJSON } = render(<Button variant="ghost">Ghost</Button>);
|
|
43
|
+
expect(toJSON()).toMatchSnapshot();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders danger variant correctly", () => {
|
|
47
|
+
const { toJSON } = render(<Button variant="danger">Danger</Button>);
|
|
48
|
+
expect(toJSON()).toMatchSnapshot();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("renders small size correctly", () => {
|
|
52
|
+
const { toJSON } = render(<Button size="sm">Small</Button>);
|
|
53
|
+
expect(toJSON()).toMatchSnapshot();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("renders large size correctly", () => {
|
|
57
|
+
const { toJSON } = render(<Button size="lg">Large</Button>);
|
|
58
|
+
expect(toJSON()).toMatchSnapshot();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("renders loading state correctly", () => {
|
|
62
|
+
const { toJSON } = render(<Button isLoading>Loading</Button>);
|
|
63
|
+
expect(toJSON()).toMatchSnapshot();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("renders disabled state correctly", () => {
|
|
67
|
+
const { toJSON } = render(<Button disabled>Disabled</Button>);
|
|
68
|
+
expect(toJSON()).toMatchSnapshot();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("renders with custom children correctly", () => {
|
|
72
|
+
const { toJSON } = render(
|
|
73
|
+
<Button>
|
|
74
|
+
<View>
|
|
75
|
+
<Text>Custom Content</Text>
|
|
76
|
+
</View>
|
|
77
|
+
</Button>
|
|
78
|
+
);
|
|
79
|
+
expect(toJSON()).toMatchSnapshot();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("Card", () => {
|
|
84
|
+
it("renders default variant correctly", () => {
|
|
85
|
+
const { toJSON } = render(
|
|
86
|
+
<Card>
|
|
87
|
+
<Text>Default Card</Text>
|
|
88
|
+
</Card>
|
|
89
|
+
);
|
|
90
|
+
expect(toJSON()).toMatchSnapshot();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("renders elevated variant correctly", () => {
|
|
94
|
+
const { toJSON } = render(
|
|
95
|
+
<Card variant="elevated">
|
|
96
|
+
<Text>Elevated Card</Text>
|
|
97
|
+
</Card>
|
|
98
|
+
);
|
|
99
|
+
expect(toJSON()).toMatchSnapshot();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("renders outlined variant correctly", () => {
|
|
103
|
+
const { toJSON } = render(
|
|
104
|
+
<Card variant="outlined">
|
|
105
|
+
<Text>Outlined Card</Text>
|
|
106
|
+
</Card>
|
|
107
|
+
);
|
|
108
|
+
expect(toJSON()).toMatchSnapshot();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("renders with custom className correctly", () => {
|
|
112
|
+
const { toJSON } = render(
|
|
113
|
+
<Card className="p-4 m-2">
|
|
114
|
+
<Text>Custom Styled Card</Text>
|
|
115
|
+
</Card>
|
|
116
|
+
);
|
|
117
|
+
expect(toJSON()).toMatchSnapshot();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("renders complex content correctly", () => {
|
|
121
|
+
const { toJSON } = render(
|
|
122
|
+
<Card variant="elevated" className="p-4">
|
|
123
|
+
<Text>Title</Text>
|
|
124
|
+
<Text>Description text here</Text>
|
|
125
|
+
<Button>Action</Button>
|
|
126
|
+
</Card>
|
|
127
|
+
);
|
|
128
|
+
expect(toJSON()).toMatchSnapshot();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Reusable accessibility test helpers
|
|
3
|
+
* Provides assertion functions to verify a11y compliance of UI components.
|
|
4
|
+
* @module __tests__/helpers/a11y
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ReactTestInstance } from "react-test-renderer";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Assert that an element is an accessible button.
|
|
11
|
+
* Checks: accessibilityRole="button" or role="button", has accessibilityLabel
|
|
12
|
+
*/
|
|
13
|
+
export function expectAccessibleButton(element: ReactTestInstance): void {
|
|
14
|
+
// Check role is "button"
|
|
15
|
+
const role = element.props.accessibilityRole || element.props.role;
|
|
16
|
+
expect(role).toBe("button");
|
|
17
|
+
// Check has label (accessibilityLabel or aria-label)
|
|
18
|
+
const label = element.props.accessibilityLabel || element.props["aria-label"];
|
|
19
|
+
expect(label).toBeTruthy();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Assert that an element is an accessible text input.
|
|
24
|
+
* Checks: has accessibilityLabel or placeholder, correct state (editable)
|
|
25
|
+
*/
|
|
26
|
+
export function expectAccessibleInput(element: ReactTestInstance): void {
|
|
27
|
+
const label =
|
|
28
|
+
element.props.accessibilityLabel ||
|
|
29
|
+
element.props["aria-label"] ||
|
|
30
|
+
element.props.placeholder;
|
|
31
|
+
expect(label).toBeTruthy();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Assert that an element has an accessible label.
|
|
36
|
+
*/
|
|
37
|
+
export function expectAccessibleLabel(element: ReactTestInstance): void {
|
|
38
|
+
const label =
|
|
39
|
+
element.props.accessibilityLabel ||
|
|
40
|
+
element.props["aria-label"] ||
|
|
41
|
+
element.props.accessibilityHint;
|
|
42
|
+
expect(label).toBeTruthy();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Assert that a disabled element communicates its disabled state to assistive tech.
|
|
47
|
+
*/
|
|
48
|
+
export function expectAccessibleDisabledState(
|
|
49
|
+
element: ReactTestInstance
|
|
50
|
+
): void {
|
|
51
|
+
const state = element.props.accessibilityState;
|
|
52
|
+
expect(state).toBeDefined();
|
|
53
|
+
expect(state.disabled).toBe(true);
|
|
54
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react-native";
|
|
2
|
+
import { usePathname } from "expo-router";
|
|
3
|
+
|
|
4
|
+
import { useTrackScreen } from "@/hooks/useTrackScreen";
|
|
5
|
+
import { useTrackEvent } from "@/hooks/useTrackEvent";
|
|
6
|
+
import { Analytics } from "@/services/analytics/analytics-adapter";
|
|
7
|
+
|
|
8
|
+
// Mock analytics adapter
|
|
9
|
+
jest.mock("@/services/analytics/analytics-adapter", () => ({
|
|
10
|
+
Analytics: {
|
|
11
|
+
screen: jest.fn(),
|
|
12
|
+
track: jest.fn(),
|
|
13
|
+
identify: jest.fn(),
|
|
14
|
+
reset: jest.fn(),
|
|
15
|
+
configure: jest.fn(),
|
|
16
|
+
initialize: jest.fn(),
|
|
17
|
+
setUserProperties: jest.fn(),
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const mockAnalytics = Analytics as jest.Mocked<typeof Analytics>;
|
|
22
|
+
const mockUsePathname = usePathname as jest.Mock;
|
|
23
|
+
|
|
24
|
+
describe("useTrackScreen", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
mockUsePathname.mockReturnValue("/home");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should call Analytics.screen when pathname changes", () => {
|
|
31
|
+
mockUsePathname.mockReturnValue("/home");
|
|
32
|
+
const { rerender } = renderHook(() => useTrackScreen());
|
|
33
|
+
|
|
34
|
+
expect(mockAnalytics.screen).toHaveBeenCalledWith("/home", {
|
|
35
|
+
segments: [],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Change pathname
|
|
39
|
+
mockUsePathname.mockReturnValue("/profile");
|
|
40
|
+
rerender({});
|
|
41
|
+
|
|
42
|
+
expect(mockAnalytics.screen).toHaveBeenCalledWith("/profile", {
|
|
43
|
+
segments: [],
|
|
44
|
+
});
|
|
45
|
+
expect(mockAnalytics.screen).toHaveBeenCalledTimes(2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should not call Analytics.screen with empty pathname", () => {
|
|
49
|
+
mockUsePathname.mockReturnValue("");
|
|
50
|
+
renderHook(() => useTrackScreen());
|
|
51
|
+
|
|
52
|
+
expect(mockAnalytics.screen).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should not fire duplicate events for same pathname", () => {
|
|
56
|
+
mockUsePathname.mockReturnValue("/home");
|
|
57
|
+
const { rerender } = renderHook(() => useTrackScreen());
|
|
58
|
+
|
|
59
|
+
expect(mockAnalytics.screen).toHaveBeenCalledTimes(1);
|
|
60
|
+
|
|
61
|
+
// Re-render with same pathname
|
|
62
|
+
rerender({});
|
|
63
|
+
|
|
64
|
+
expect(mockAnalytics.screen).toHaveBeenCalledTimes(1);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("useTrackEvent", () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
jest.clearAllMocks();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should return a track function", () => {
|
|
74
|
+
const { result } = renderHook(() => useTrackEvent());
|
|
75
|
+
|
|
76
|
+
expect(typeof result.current.track).toBe("function");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should call Analytics.track with event name and properties", () => {
|
|
80
|
+
const { result } = renderHook(() => useTrackEvent());
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
result.current.track("Button Clicked", { buttonId: "submit" });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(mockAnalytics.track).toHaveBeenCalledWith("Button Clicked", {
|
|
87
|
+
buttonId: "submit",
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should return memoized track function (same reference across renders)", () => {
|
|
92
|
+
const { result, rerender } = renderHook(() => useTrackEvent());
|
|
93
|
+
|
|
94
|
+
const firstTrack = result.current.track;
|
|
95
|
+
rerender({});
|
|
96
|
+
const secondTrack = result.current.track;
|
|
97
|
+
|
|
98
|
+
expect(firstTrack).toBe(secondTrack);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react-native";
|
|
2
|
+
|
|
3
|
+
import { useAnimatedEntry, useStaggeredEntry } from "@/hooks/useAnimatedEntry";
|
|
4
|
+
import { useParallax } from "@/hooks/useParallax";
|
|
5
|
+
|
|
6
|
+
describe("useAnimatedEntry", () => {
|
|
7
|
+
it("should return animatedStyle and play function", () => {
|
|
8
|
+
const { result } = renderHook(() => useAnimatedEntry());
|
|
9
|
+
|
|
10
|
+
expect(result.current.animatedStyle).toBeDefined();
|
|
11
|
+
expect(typeof result.current.play).toBe("function");
|
|
12
|
+
expect(typeof result.current.reset).toBe("function");
|
|
13
|
+
expect(result.current.progress).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should default animation type to fadeIn", () => {
|
|
17
|
+
// The hook should work without options and use fadeIn as default
|
|
18
|
+
const { result } = renderHook(() => useAnimatedEntry());
|
|
19
|
+
|
|
20
|
+
// The hook returns successfully with defaults
|
|
21
|
+
expect(result.current.animatedStyle).toBeDefined();
|
|
22
|
+
expect(result.current.progress).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should accept custom animation options", () => {
|
|
26
|
+
const { result } = renderHook(() =>
|
|
27
|
+
useAnimatedEntry({
|
|
28
|
+
animation: "slideUp",
|
|
29
|
+
delay: 200,
|
|
30
|
+
autoPlay: false,
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(result.current.animatedStyle).toBeDefined();
|
|
35
|
+
expect(typeof result.current.play).toBe("function");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("useStaggeredEntry", () => {
|
|
40
|
+
it("should return staggered animated style with index-based delay", () => {
|
|
41
|
+
const { result } = renderHook(() =>
|
|
42
|
+
useStaggeredEntry(2, { animation: "slideUp", staggerDelay: 80 })
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(result.current.animatedStyle).toBeDefined();
|
|
46
|
+
expect(typeof result.current.play).toBe("function");
|
|
47
|
+
expect(typeof result.current.reset).toBe("function");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("useParallax", () => {
|
|
52
|
+
it("should return scrollY, scrollHandler, parallaxStyle, and headerStyle", () => {
|
|
53
|
+
const { result } = renderHook(() => useParallax());
|
|
54
|
+
|
|
55
|
+
expect(result.current.scrollY).toBeDefined();
|
|
56
|
+
expect(result.current.scrollHandler).toBeDefined();
|
|
57
|
+
expect(result.current.parallaxStyle).toBeDefined();
|
|
58
|
+
expect(result.current.headerStyle).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should accept custom options", () => {
|
|
62
|
+
const { result } = renderHook(() =>
|
|
63
|
+
useParallax({ speed: 0.3, headerHeight: 300 })
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(result.current.scrollY).toBeDefined();
|
|
67
|
+
expect(result.current.parallaxStyle).toBeDefined();
|
|
68
|
+
expect(result.current.headerStyle).toBeDefined();
|
|
69
|
+
});
|
|
70
|
+
});
|