@umituz/react-native-splash 1.6.3 → 1.7.1

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 (42) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/lib/__tests__/mocks/expoLinearGradient.js +7 -0
  4. package/lib/__tests__/mocks/reactNative.js +16 -0
  5. package/lib/__tests__/setup.ts +57 -0
  6. package/lib/__tests__/utils/testUtils.tsx +86 -0
  7. package/lib/domain/entities/SplashOptions.ts +74 -0
  8. package/lib/index.ts +31 -0
  9. package/lib/presentation/components/SplashDecorations.tsx +56 -0
  10. package/lib/presentation/components/SplashErrorBoundary.tsx +63 -0
  11. package/lib/presentation/components/SplashLoading.tsx +74 -0
  12. package/lib/presentation/components/SplashLogo.tsx +80 -0
  13. package/lib/presentation/components/SplashScreen.tsx +175 -0
  14. package/lib/presentation/components/SplashTypography.tsx +72 -0
  15. package/lib/presentation/hooks/useSplash.ts +70 -0
  16. package/lib/presentation/utils/splashGradient.utils.ts +47 -0
  17. package/lib/types/expo-linear-gradient.d.ts +12 -0
  18. package/package.json +18 -5
  19. package/src/__tests__/SplashScreen.test.tsx +161 -0
  20. package/src/__tests__/accessibility/Accessibility.test.tsx +264 -0
  21. package/src/__tests__/basic/Basic.test.tsx +106 -0
  22. package/src/__tests__/basic/Simple.test.tsx +32 -0
  23. package/src/__tests__/edge-cases/EdgeCases.test.tsx +446 -0
  24. package/src/__tests__/integration/SplashScreen.integration.test.tsx +200 -0
  25. package/src/__tests__/mocks/expoLinearGradient.js +7 -0
  26. package/src/__tests__/mocks/reactNative.js +16 -0
  27. package/src/__tests__/performance/Performance.test.tsx +297 -0
  28. package/src/__tests__/setup.ts +57 -0
  29. package/src/__tests__/useSplash.test.tsx +123 -0
  30. package/src/__tests__/utils/testUtils.tsx +86 -0
  31. package/src/__tests__/visual/VisualRegression.test.tsx +338 -0
  32. package/src/domain/entities/SplashOptions.ts +7 -0
  33. package/src/index.ts +2 -0
  34. package/src/presentation/components/SplashDecorations.tsx +13 -5
  35. package/src/presentation/components/SplashErrorBoundary.tsx +63 -0
  36. package/src/presentation/components/SplashLoading.tsx +7 -5
  37. package/src/presentation/components/SplashLogo.tsx +4 -2
  38. package/src/presentation/components/SplashScreen.tsx +50 -30
  39. package/src/presentation/components/SplashTypography.tsx +6 -4
  40. package/src/presentation/hooks/useSplash.ts +70 -0
  41. package/src/presentation/utils/splashGradient.utils.ts +0 -0
  42. package/src/types/expo-linear-gradient.d.ts +12 -0
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Performance Tests - Memory Leak Prevention
3
+ */
4
+
5
+ import React from 'react';
6
+ import { render, act, trackMemoryUsage } from '../utils/testUtils';
7
+ import { SplashScreen } from '../../presentation/components/SplashScreen';
8
+ import { useSplash } from '../../presentation/hooks/useSplash';
9
+
10
+ describe('Performance Tests', () => {
11
+ beforeEach(() => {
12
+ jest.useFakeTimers();
13
+ });
14
+
15
+ afterEach(() => {
16
+ jest.useRealTimers();
17
+ });
18
+
19
+ describe('Memory Leak Prevention', () => {
20
+ it('prevents memory leaks with timer cleanup', () => {
21
+ const { trackMemoryUsage } = require('../utils/testUtils');
22
+ const memoryTracker = trackMemoryUsage();
23
+
24
+ const components = [];
25
+
26
+ // Create and destroy many components
27
+ for (let i = 0; i < 100; i++) {
28
+ const { unmount } = render(
29
+ <SplashScreen
30
+ appName={`App ${i}`}
31
+ visible={true}
32
+ minimumDisplayTime={1000}
33
+ />
34
+ );
35
+ components.push(unmount);
36
+ }
37
+
38
+ // Unmount all components
39
+ components.forEach(unmount => unmount());
40
+
41
+ // Force garbage collection if available
42
+ if (global.gc) {
43
+ global.gc();
44
+ }
45
+
46
+ const memoryIncrease = memoryTracker.getMemoryIncrease();
47
+
48
+ // Memory increase should be minimal after cleanup
49
+ expect(memoryIncrease).toBeLessThan(5 * 1024 * 1024); // 5MB
50
+ });
51
+
52
+ it('cleans up useSplash timers properly', () => {
53
+ const TestComponent = () => {
54
+ const { isVisible, markReady } = useSplash({
55
+ minimumDisplayTime: 5000,
56
+ autoHide: true,
57
+ });
58
+
59
+ return (
60
+ <SplashScreen
61
+ appName="Test App"
62
+ visible={isVisible}
63
+ onReady={markReady}
64
+ />
65
+ );
66
+ };
67
+
68
+ const { unmount } = render(<TestComponent />);
69
+
70
+ // Component should be unmounted before timer completes
71
+ unmount();
72
+
73
+ // Should not cause memory leaks or timer issues
74
+ expect(true).toBe(true);
75
+ });
76
+
77
+ it('handles rapid mount/unmount cycles', () => {
78
+ const { trackMemoryUsage } = require('../utils/testUtils');
79
+ const memoryTracker = trackMemoryUsage();
80
+
81
+ for (let i = 0; i < 50; i++) {
82
+ const { unmount } = render(
83
+ <SplashScreen
84
+ appName="Rapid Test"
85
+ visible={true}
86
+ minimumDisplayTime={100}
87
+ />
88
+ );
89
+
90
+ // Immediately unmount
91
+ unmount();
92
+ }
93
+
94
+ if (global.gc) {
95
+ global.gc();
96
+ }
97
+
98
+ const memoryIncrease = memoryTracker.getMemoryIncrease();
99
+ expect(memoryIncrease).toBeLessThan(2 * 1024 * 1024); // 2MB
100
+ });
101
+ });
102
+
103
+ describe('Render Performance', () => {
104
+ it('renders quickly with basic props', async () => {
105
+ const { measureRenderTime } = require('../utils/testUtils');
106
+
107
+ const renderTime = await measureRenderTime(
108
+ <SplashScreen
109
+ appName="Performance Test"
110
+ visible={true}
111
+ />
112
+ );
113
+
114
+ expect(renderTime).toBeLessThan(50); // 50ms
115
+ });
116
+
117
+ it('renders efficiently with complex props', async () => {
118
+ const { measureRenderTime } = require('../utils/testUtils');
119
+
120
+ const ComplexSplash = () => (
121
+ <SplashScreen
122
+ appName="Complex App"
123
+ tagline="Complex tagline with lots of text"
124
+ gradientColors={['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF']}
125
+ textColor="#FFFFFF"
126
+ iconColor="#FFFF00"
127
+ decorationColor="rgba(255, 255, 255, 0.1)"
128
+ showLoading={true}
129
+ minimumDisplayTime={2000}
130
+ renderLogo={() => <div>Complex Logo</div>}
131
+ renderContent={() => <div>Complex Content</div>}
132
+ renderFooter={() => <div>Complex Footer</div>}
133
+ visible={true}
134
+ />
135
+ );
136
+
137
+ const renderTime = await measureRenderTime(<ComplexSplash />);
138
+
139
+ expect(renderTime).toBeLessThan(100); // 100ms
140
+ });
141
+
142
+ it('handles prop updates efficiently', async () => {
143
+ const { measureRenderTime } = require('../utils/testUtils');
144
+
145
+ let props = {
146
+ appName: "Initial App",
147
+ visible: true,
148
+ };
149
+
150
+ const initialRenderTime = await measureRenderTime(
151
+ <SplashScreen {...props} />
152
+ );
153
+
154
+ // Update props
155
+ props = {
156
+ ...props,
157
+ appName: "Updated App",
158
+ textColor: "#FF0000",
159
+ };
160
+
161
+ const updateRenderTime = await measureRenderTime(
162
+ <SplashScreen {...props} />
163
+ );
164
+
165
+ expect(initialRenderTime).toBeLessThan(50);
166
+ expect(updateRenderTime).toBeLessThan(30); // Updates should be faster
167
+ });
168
+ });
169
+
170
+ describe('Timer Performance', () => {
171
+ it('handles multiple timers efficiently', () => {
172
+ const onReady = jest.fn();
173
+
174
+ const { unmount } = render(
175
+ <SplashScreen
176
+ appName="Timer Test"
177
+ visible={true}
178
+ minimumDisplayTime={1000}
179
+ onReady={onReady}
180
+ />
181
+ );
182
+
183
+ // Advance timers partially
184
+ act(() => {
185
+ jest.advanceTimersByTime(500);
186
+ });
187
+
188
+ expect(onReady).not.toHaveBeenCalled();
189
+
190
+ // Complete timers
191
+ act(() => {
192
+ jest.advanceTimersByTime(500);
193
+ });
194
+
195
+ expect(onReady).toHaveBeenCalledTimes(1);
196
+
197
+ unmount();
198
+ });
199
+
200
+ it('prevents timer conflicts', () => {
201
+ const onReady1 = jest.fn();
202
+ const onReady2 = jest.fn();
203
+
204
+ const { unmount: unmount1 } = render(
205
+ <SplashScreen
206
+ appName="Timer Test 1"
207
+ visible={true}
208
+ minimumDisplayTime={1000}
209
+ onReady={onReady1}
210
+ />
211
+ );
212
+
213
+ const { unmount: unmount2 } = render(
214
+ <SplashScreen
215
+ appName="Timer Test 2"
216
+ visible={true}
217
+ minimumDisplayTime={1500}
218
+ onReady={onReady2}
219
+ />
220
+ );
221
+
222
+ act(() => {
223
+ jest.advanceTimersByTime(1000);
224
+ });
225
+
226
+ expect(onReady1).toHaveBeenCalledTimes(1);
227
+ expect(onReady2).not.toHaveBeenCalled();
228
+
229
+ act(() => {
230
+ jest.advanceTimersByTime(500);
231
+ });
232
+
233
+ expect(onReady2).toHaveBeenCalledTimes(1);
234
+
235
+ unmount1();
236
+ unmount2();
237
+ });
238
+ });
239
+
240
+ describe('Memory Usage Patterns', () => {
241
+ it('maintains stable memory usage during extended operation', () => {
242
+ const { trackMemoryUsage } = require('../utils/testUtils');
243
+ const memoryTracker = trackMemoryUsage();
244
+
245
+ let currentProps = {
246
+ appName: "Base App",
247
+ visible: true,
248
+ };
249
+
250
+ const { rerender } = render(<SplashScreen {...currentProps} />);
251
+
252
+ // Simulate extended operation with prop changes
253
+ for (let i = 0; i < 100; i++) {
254
+ currentProps = {
255
+ ...currentProps,
256
+ appName: `App ${i}`,
257
+ tagline: i % 2 === 0 ? `Tagline ${i}` : undefined,
258
+ };
259
+
260
+ rerender(<SplashScreen {...currentProps} />);
261
+
262
+ if (i % 10 === 0 && global.gc) {
263
+ global.gc();
264
+ }
265
+ }
266
+
267
+ const memoryIncrease = memoryTracker.getMemoryIncrease();
268
+ expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024); // 10MB
269
+ });
270
+
271
+ it('handles large prop objects efficiently', () => {
272
+ const largeGradientColors = Array.from({ length: 100 }, (_, i) =>
273
+ `#${i.toString(16).padStart(6, '0')}`
274
+ );
275
+
276
+ const { trackMemoryUsage } = require('../utils/testUtils');
277
+ const memoryTracker = trackMemoryUsage();
278
+
279
+ const { unmount } = render(
280
+ <SplashScreen
281
+ appName="Large Props Test"
282
+ gradientColors={largeGradientColors}
283
+ visible={true}
284
+ />
285
+ );
286
+
287
+ unmount();
288
+
289
+ if (global.gc) {
290
+ global.gc();
291
+ }
292
+
293
+ const memoryIncrease = memoryTracker.getMemoryIncrease();
294
+ expect(memoryIncrease).toBeLessThan(1 * 1024 * 1024); // 1MB
295
+ });
296
+ });
297
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Simple Test Setup
3
+ */
4
+
5
+ // Mock React Native before anything else
6
+ jest.mock('react-native', () => {
7
+ const React = require('react');
8
+ return {
9
+ View: ({ children, testID, style }) => React.createElement('View', { testID, style }, children),
10
+ Text: ({ children, testID, style }) => React.createElement('Text', { testID, style }, children),
11
+ StyleSheet: {
12
+ create: jest.fn((styles) => styles),
13
+ flatten: jest.fn((style) => style)
14
+ },
15
+ Dimensions: { get: jest.fn(() => ({ width: 375, height: 667 })) },
16
+ LinearGradient: ({ children, testID, style }) => React.createElement('LinearGradient', { testID, style }, children),
17
+ };
18
+ });
19
+
20
+ // Mock expo-linear-gradient
21
+ jest.mock('expo-linear-gradient', () => {
22
+ const React = require('react');
23
+ return {
24
+ LinearGradient: ({ children, testID, style }) => React.createElement('LinearGradient', { testID, style }, children),
25
+ };
26
+ });
27
+
28
+ // Mock design system atoms
29
+ jest.mock('@umituz/react-native-design-system-atoms', () => {
30
+ const React = require('react');
31
+ return {
32
+ AtomicIcon: ({ name, testID }) => React.createElement('AtomicIcon', { testID: testID || `icon-${name}`, name }),
33
+ };
34
+ });
35
+
36
+ // Mock design system theme
37
+ jest.mock('@umituz/react-native-design-system-theme', () => ({
38
+ useAppDesignTokens: () => ({
39
+ colors: { primary: '#007AFF' },
40
+ spacing: { xl: 24, md: 16, xxxl: 48 },
41
+ }),
42
+ }));
43
+
44
+ // Mock localization
45
+ jest.mock('@umituz/react-native-localization', () => ({
46
+ useLocalization: () => ({
47
+ t: (key: string, fallback: string) => fallback || key,
48
+ }),
49
+ }));
50
+
51
+ // Mock safe area context
52
+ jest.mock('react-native-safe-area-context', () => ({
53
+ useSafeAreaInsets: () => ({ top: 44, bottom: 34, left: 0, right: 0 }),
54
+ }));
55
+
56
+ // Global test utilities
57
+ global.__DEV__ = true;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Use Splash Hook Tests
3
+ */
4
+
5
+ import { renderHook, act } from "@testing-library/react-hooks";
6
+ import { useSplash } from "../presentation/hooks/useSplash";
7
+
8
+ describe("useSplash", () => {
9
+ beforeEach(() => {
10
+ jest.useFakeTimers();
11
+ });
12
+
13
+ afterEach(() => {
14
+ jest.useRealTimers();
15
+ });
16
+
17
+ it("initializes with visible state", () => {
18
+ const { result } = renderHook(() => useSplash());
19
+
20
+ expect(result.current.isVisible).toBe(true);
21
+ expect(result.current.isReady).toBe(false);
22
+ });
23
+
24
+ it("marks as ready and calls onReady", async () => {
25
+ const onReady = jest.fn();
26
+ const { result } = renderHook(() => useSplash({ onReady }));
27
+
28
+ await act(async () => {
29
+ await result.current.markReady();
30
+ });
31
+
32
+ expect(result.current.isReady).toBe(true);
33
+ expect(onReady).toHaveBeenCalledTimes(1);
34
+ });
35
+
36
+ it("auto-hides after minimum display time", async () => {
37
+ const { result } = renderHook(() => useSplash({ minimumDisplayTime: 1000 }));
38
+
39
+ await act(async () => {
40
+ await result.current.markReady();
41
+ });
42
+
43
+ expect(result.current.isVisible).toBe(true);
44
+
45
+ act(() => {
46
+ jest.advanceTimersByTime(1000);
47
+ });
48
+
49
+ expect(result.current.isVisible).toBe(false);
50
+ });
51
+
52
+ it("does not auto-hide when autoHide is false", async () => {
53
+ const { result } = renderHook(() => useSplash({ autoHide: false }));
54
+
55
+ await act(async () => {
56
+ await result.current.markReady();
57
+ });
58
+
59
+ act(() => {
60
+ jest.advanceTimersByTime(2000);
61
+ });
62
+
63
+ expect(result.current.isVisible).toBe(true);
64
+ });
65
+
66
+ it("can manually hide and show", () => {
67
+ const { result } = renderHook(() => useSplash());
68
+
69
+ act(() => {
70
+ result.current.hide();
71
+ });
72
+
73
+ expect(result.current.isVisible).toBe(false);
74
+
75
+ act(() => {
76
+ result.current.show();
77
+ });
78
+
79
+ expect(result.current.isVisible).toBe(true);
80
+ });
81
+
82
+ it("prevents multiple ready calls", async () => {
83
+ const onReady = jest.fn();
84
+ const { result } = renderHook(() => useSplash({ onReady }));
85
+
86
+ await act(async () => {
87
+ await result.current.markReady();
88
+ await result.current.markReady();
89
+ await result.current.markReady();
90
+ });
91
+
92
+ expect(onReady).toHaveBeenCalledTimes(1);
93
+ expect(result.current.isReady).toBe(true);
94
+ });
95
+
96
+ it("handles onReady errors gracefully", async () => {
97
+ const consoleError = jest.spyOn(console, "error").mockImplementation();
98
+ const onReady = jest.fn().mockRejectedValue(new Error("Test error"));
99
+ const { result } = renderHook(() => useSplash({ onReady }));
100
+
101
+ await act(async () => {
102
+ await result.current.markReady();
103
+ });
104
+
105
+ expect(result.current.isReady).toBe(true);
106
+ expect(consoleError).toHaveBeenCalled();
107
+
108
+ consoleError.mockRestore();
109
+ });
110
+
111
+ it("cleans up timer on unmount", () => {
112
+ const { result, unmount } = renderHook(() => useSplash());
113
+
114
+ act(() => {
115
+ result.current.markReady();
116
+ });
117
+
118
+ unmount();
119
+
120
+ // Should not throw error about timer cleanup
121
+ expect(true).toBe(true);
122
+ });
123
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Test Utilities and Mocks
3
+ */
4
+
5
+ import React from 'react';
6
+ import { render, RenderOptions } from '@testing-library/react-native';
7
+ import { View } from 'react-native';
8
+
9
+ // Custom render function with providers
10
+ const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
11
+ return <View>{children}</View>;
12
+ };
13
+
14
+ const customRender = (
15
+ ui: React.ReactElement,
16
+ options?: Omit<RenderOptions, 'wrapper'>
17
+ ) => render(ui, { wrapper: AllTheProviders, ...options });
18
+
19
+ // Mock timers utilities
20
+ export const advanceTimersByTime = (ms: number) => {
21
+ jest.advanceTimersByTime(ms);
22
+ };
23
+
24
+ export const runAllTimers = () => {
25
+ jest.runAllTimers();
26
+ };
27
+
28
+ // Mock data generators
29
+ export const createMockSplashProps = (overrides = {}) => ({
30
+ appName: 'Test App',
31
+ tagline: 'Test Tagline',
32
+ visible: true,
33
+ showLoading: true,
34
+ minimumDisplayTime: 1500,
35
+ ...overrides,
36
+ });
37
+
38
+ export const createMockDesignTokens = (overrides = {}) => ({
39
+ colors: { primary: '#007AFF' },
40
+ spacing: { xl: 24, md: 16, xxxl: 48 },
41
+ ...overrides,
42
+ });
43
+
44
+ // Performance testing utilities
45
+ export const measureRenderTime = async (component: React.ReactElement) => {
46
+ const start = performance.now();
47
+ customRender(component);
48
+ const end = performance.now();
49
+ return end - start;
50
+ };
51
+
52
+ // Memory leak testing utilities
53
+ export const trackMemoryUsage = () => {
54
+ const initialMemory = (performance as any).memory?.usedJSHeapSize || 0;
55
+ return {
56
+ getMemoryIncrease: () => {
57
+ const currentMemory = (performance as any).memory?.usedJSHeapSize || 0;
58
+ return currentMemory - initialMemory;
59
+ },
60
+ };
61
+ };
62
+
63
+ // Accessibility testing utilities
64
+ export const checkAccessibility = (container: any) => {
65
+ const accessibilityElements = container.findAllByProps({
66
+ accessible: true,
67
+ });
68
+
69
+ return {
70
+ hasAccessibleElements: accessibilityElements.length > 0,
71
+ accessibleElementCount: accessibilityElements.length,
72
+ elements: accessibilityElements,
73
+ };
74
+ };
75
+
76
+ // Error boundary testing utilities
77
+ export const createErrorComponent = (errorMessage: string) => {
78
+ const ThrowError = () => {
79
+ throw new Error(errorMessage);
80
+ };
81
+ return ThrowError;
82
+ };
83
+
84
+ // Re-export testing library
85
+ export * from '@testing-library/react-native';
86
+ export { customRender as render };