@nextsparkjs/plugin-amplitude 0.1.0-beta.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.
- package/CODE_REVIEW_REPORT.md +462 -0
- package/README.md +619 -0
- package/__tests__/amplitude-core.test.ts +279 -0
- package/__tests__/hooks.test.ts +478 -0
- package/__tests__/validation.test.ts +393 -0
- package/components/AnalyticsDashboard.tsx +339 -0
- package/components/ConsentManager.tsx +265 -0
- package/components/ExperimentWrapper.tsx +440 -0
- package/components/PerformanceMonitor.tsx +578 -0
- package/hooks/useAmplitude.ts +132 -0
- package/hooks/useAmplitudeEvents.ts +100 -0
- package/hooks/useExperiment.ts +195 -0
- package/hooks/useSessionReplay.ts +238 -0
- package/jest.setup.ts +276 -0
- package/lib/amplitude-core.ts +178 -0
- package/lib/cache.ts +181 -0
- package/lib/performance.ts +319 -0
- package/lib/queue.ts +389 -0
- package/lib/security.ts +188 -0
- package/package.json +15 -0
- package/plugin.config.ts +58 -0
- package/providers/AmplitudeProvider.tsx +113 -0
- package/styles/amplitude.css +593 -0
- package/translations/en.json +45 -0
- package/translations/es.json +45 -0
- package/tsconfig.json +47 -0
- package/types/amplitude.types.ts +105 -0
- package/utils/debounce.ts +133 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test suite for Amplitude hooks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook, act } from '@testing-library/react';
|
|
6
|
+
import { useAmplitude } from '../hooks/useAmplitude';
|
|
7
|
+
import { useExperiment } from '../hooks/useExperiment';
|
|
8
|
+
import { useSessionReplay } from '../hooks/useSessionReplay';
|
|
9
|
+
import { AmplitudeProvider } from '../providers/AmplitudeProvider';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
|
|
12
|
+
// Mock context
|
|
13
|
+
const mockAmplitudeContext = {
|
|
14
|
+
amplitude: {
|
|
15
|
+
track: jest.fn().mockResolvedValue(undefined),
|
|
16
|
+
identify: jest.fn().mockResolvedValue(undefined),
|
|
17
|
+
setUserProperties: jest.fn().mockResolvedValue(undefined),
|
|
18
|
+
reset: jest.fn(),
|
|
19
|
+
isInitialized: jest.fn().mockReturnValue(true),
|
|
20
|
+
},
|
|
21
|
+
isInitialized: true,
|
|
22
|
+
config: {
|
|
23
|
+
apiKey: 'test-key',
|
|
24
|
+
serverZone: 'US' as const,
|
|
25
|
+
enableSessionReplay: true,
|
|
26
|
+
enableABTesting: true,
|
|
27
|
+
sampleRate: 1,
|
|
28
|
+
enableConsentManagement: true,
|
|
29
|
+
batchSize: 30,
|
|
30
|
+
flushInterval: 10000,
|
|
31
|
+
debugMode: false,
|
|
32
|
+
piiMaskingEnabled: true,
|
|
33
|
+
rateLimitEventsPerMinute: 1000,
|
|
34
|
+
errorRetryAttempts: 3,
|
|
35
|
+
errorRetryDelayMs: 1000,
|
|
36
|
+
},
|
|
37
|
+
consent: {
|
|
38
|
+
analytics: true,
|
|
39
|
+
sessionReplay: true,
|
|
40
|
+
experiments: true,
|
|
41
|
+
performance: true,
|
|
42
|
+
},
|
|
43
|
+
updateConsent: jest.fn(),
|
|
44
|
+
error: null,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Mock the context hook
|
|
48
|
+
jest.mock('../providers/AmplitudeProvider', () => ({
|
|
49
|
+
useAmplitudeContext: () => mockAmplitudeContext,
|
|
50
|
+
AmplitudeProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// Mock performance tracking
|
|
54
|
+
jest.mock('../lib/performance', () => ({
|
|
55
|
+
trackPerformanceMetric: jest.fn(),
|
|
56
|
+
getPerformanceStats: jest.fn(() => ({
|
|
57
|
+
amplitudeCore: {
|
|
58
|
+
initTime: 100,
|
|
59
|
+
trackingLatency: [50, 60, 70],
|
|
60
|
+
errorRate: 0.01,
|
|
61
|
+
eventQueueSize: 5,
|
|
62
|
+
memoryUsage: 1024 * 1024,
|
|
63
|
+
},
|
|
64
|
+
webVitals: {
|
|
65
|
+
cls: 0.1,
|
|
66
|
+
fid: 50,
|
|
67
|
+
fcp: 1000,
|
|
68
|
+
lcp: 2000,
|
|
69
|
+
ttfb: 200,
|
|
70
|
+
inp: 100,
|
|
71
|
+
},
|
|
72
|
+
browserMetrics: {
|
|
73
|
+
memoryUsage: 1024 * 1024,
|
|
74
|
+
connectionType: '4g',
|
|
75
|
+
devicePixelRatio: 2,
|
|
76
|
+
screenResolution: '1920x1080',
|
|
77
|
+
viewportSize: '1200x800',
|
|
78
|
+
},
|
|
79
|
+
})),
|
|
80
|
+
getPerformanceMetrics: jest.fn(() => []),
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// Mock security audit logger
|
|
84
|
+
jest.mock('../lib/security', () => ({
|
|
85
|
+
auditLogger: {
|
|
86
|
+
log: jest.fn(),
|
|
87
|
+
},
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
describe('useAmplitude hook', () => {
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
jest.clearAllMocks();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should provide amplitude tracking functions', () => {
|
|
96
|
+
const { result } = renderHook(() => useAmplitude());
|
|
97
|
+
|
|
98
|
+
expect(result.current).toHaveProperty('track');
|
|
99
|
+
expect(result.current).toHaveProperty('identify');
|
|
100
|
+
expect(result.current).toHaveProperty('setUserProperties');
|
|
101
|
+
expect(result.current).toHaveProperty('reset');
|
|
102
|
+
expect(result.current).toHaveProperty('isInitialized');
|
|
103
|
+
expect(result.current).toHaveProperty('context');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should track events successfully', async () => {
|
|
107
|
+
const { result } = renderHook(() => useAmplitude());
|
|
108
|
+
|
|
109
|
+
await act(async () => {
|
|
110
|
+
await result.current.track('Test Event' as any, { test: 'value' });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(mockAmplitudeContext.amplitude.track).toHaveBeenCalledWith(
|
|
114
|
+
'Test Event',
|
|
115
|
+
{ test: 'value' }
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should identify users successfully', async () => {
|
|
120
|
+
const { result } = renderHook(() => useAmplitude());
|
|
121
|
+
|
|
122
|
+
await act(async () => {
|
|
123
|
+
await result.current.identify('user123' as any, { name: 'Test User' });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(mockAmplitudeContext.amplitude.identify).toHaveBeenCalledWith(
|
|
127
|
+
'user123',
|
|
128
|
+
{ name: 'Test User' }
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should set user properties successfully', async () => {
|
|
133
|
+
const { result } = renderHook(() => useAmplitude());
|
|
134
|
+
|
|
135
|
+
await act(async () => {
|
|
136
|
+
await result.current.setUserProperties({ plan: 'premium' });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(mockAmplitudeContext.amplitude.setUserProperties).toHaveBeenCalledWith(
|
|
140
|
+
{ plan: 'premium' }
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should reset amplitude successfully', () => {
|
|
145
|
+
const { result } = renderHook(() => useAmplitude());
|
|
146
|
+
|
|
147
|
+
act(() => {
|
|
148
|
+
result.current.reset();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(mockAmplitudeContext.amplitude.reset).toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should handle errors gracefully', async () => {
|
|
155
|
+
const error = new Error('Tracking failed');
|
|
156
|
+
mockAmplitudeContext.amplitude.track.mockRejectedValueOnce(error);
|
|
157
|
+
|
|
158
|
+
const { result } = renderHook(() => useAmplitude());
|
|
159
|
+
|
|
160
|
+
await act(async () => {
|
|
161
|
+
await expect(result.current.track('Test Event' as any))
|
|
162
|
+
.rejects.toThrow('Tracking failed');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(result.current.lastError).toEqual(error);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should respect consent settings', async () => {
|
|
169
|
+
// Temporarily modify the mock context's consent
|
|
170
|
+
const originalConsent = { ...mockAmplitudeContext.consent };
|
|
171
|
+
mockAmplitudeContext.consent = {
|
|
172
|
+
analytics: false,
|
|
173
|
+
sessionReplay: false,
|
|
174
|
+
experiments: false,
|
|
175
|
+
performance: false,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const { result } = renderHook(() => useAmplitude());
|
|
179
|
+
|
|
180
|
+
await act(async () => {
|
|
181
|
+
await result.current.track('Test Event' as any);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Should not call amplitude.track when consent is not granted
|
|
185
|
+
expect(mockAmplitudeContext.amplitude.track).not.toHaveBeenCalled();
|
|
186
|
+
|
|
187
|
+
// Restore original consent
|
|
188
|
+
mockAmplitudeContext.consent = originalConsent;
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('useExperiment hook', () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
jest.clearAllMocks();
|
|
195
|
+
|
|
196
|
+
// Mock localStorage
|
|
197
|
+
Object.defineProperty(window, 'localStorage', {
|
|
198
|
+
value: {
|
|
199
|
+
getItem: jest.fn(),
|
|
200
|
+
setItem: jest.fn(),
|
|
201
|
+
removeItem: jest.fn(),
|
|
202
|
+
clear: jest.fn(),
|
|
203
|
+
},
|
|
204
|
+
writable: true,
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should provide experiment functions', () => {
|
|
209
|
+
const { result } = renderHook(() => useExperiment());
|
|
210
|
+
|
|
211
|
+
expect(result.current).toHaveProperty('getVariant');
|
|
212
|
+
expect(result.current).toHaveProperty('trackExposure');
|
|
213
|
+
expect(result.current).toHaveProperty('trackConversion');
|
|
214
|
+
expect(result.current).toHaveProperty('isInExperiment');
|
|
215
|
+
expect(result.current).toHaveProperty('registerExperiment');
|
|
216
|
+
expect(result.current).toHaveProperty('canRunExperiments');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should register experiments', () => {
|
|
220
|
+
const { result } = renderHook(() => useExperiment());
|
|
221
|
+
|
|
222
|
+
const experimentConfig = {
|
|
223
|
+
id: 'test-experiment',
|
|
224
|
+
name: 'Test Experiment',
|
|
225
|
+
description: 'A test experiment',
|
|
226
|
+
status: 'running' as const,
|
|
227
|
+
variants: [
|
|
228
|
+
{ id: 'control', name: 'Control', description: 'Control variant', allocation: 50, isControl: true, config: {} },
|
|
229
|
+
{ id: 'treatment', name: 'Treatment', description: 'Treatment variant', allocation: 50, isControl: false, config: {} },
|
|
230
|
+
],
|
|
231
|
+
targeting: {},
|
|
232
|
+
metrics: [],
|
|
233
|
+
startDate: new Date(),
|
|
234
|
+
sampleSize: 1000,
|
|
235
|
+
confidenceLevel: 0.95,
|
|
236
|
+
minimumDetectableEffect: 0.05,
|
|
237
|
+
allocations: { control: 50, treatment: 50 },
|
|
238
|
+
stickiness: 'user' as const,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
act(() => {
|
|
242
|
+
result.current.registerExperiment(experimentConfig);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(result.current.experiments.has('test-experiment')).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should track experiment exposure', async () => {
|
|
249
|
+
const { result } = renderHook(() => useExperiment());
|
|
250
|
+
|
|
251
|
+
// First register the experiment so it can be found
|
|
252
|
+
const experimentConfig = {
|
|
253
|
+
id: 'test-experiment',
|
|
254
|
+
name: 'Test Experiment',
|
|
255
|
+
description: 'A test experiment',
|
|
256
|
+
status: 'running' as const,
|
|
257
|
+
variants: [
|
|
258
|
+
{ id: 'control', name: 'Control', description: 'Control variant', allocation: 50, isControl: true, config: {} },
|
|
259
|
+
{ id: 'treatment', name: 'Treatment', description: 'Treatment variant', allocation: 50, isControl: false, config: {} },
|
|
260
|
+
],
|
|
261
|
+
targeting: {},
|
|
262
|
+
metrics: [],
|
|
263
|
+
startDate: new Date(),
|
|
264
|
+
sampleSize: 1000,
|
|
265
|
+
confidenceLevel: 0.95,
|
|
266
|
+
minimumDetectableEffect: 0.05,
|
|
267
|
+
allocations: { control: 50, treatment: 50 },
|
|
268
|
+
stickiness: 'user' as const,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
act(() => {
|
|
272
|
+
result.current.registerExperiment(experimentConfig);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await act(async () => {
|
|
276
|
+
await result.current.trackExposure('test-experiment', 'control');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(mockAmplitudeContext.amplitude.track).toHaveBeenCalledWith(
|
|
280
|
+
'Experiment Exposed',
|
|
281
|
+
expect.objectContaining({
|
|
282
|
+
experiment_id: 'test-experiment',
|
|
283
|
+
variant_id: 'control',
|
|
284
|
+
})
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should track experiment conversion', async () => {
|
|
289
|
+
const { result } = renderHook(() => useExperiment());
|
|
290
|
+
|
|
291
|
+
// Register the experiment first
|
|
292
|
+
const experimentConfig = {
|
|
293
|
+
id: 'test-experiment',
|
|
294
|
+
name: 'Test Experiment',
|
|
295
|
+
description: 'A test experiment',
|
|
296
|
+
status: 'running' as const,
|
|
297
|
+
variants: [
|
|
298
|
+
{ id: 'control', name: 'Control', description: 'Control variant', allocation: 50, isControl: true, config: {} },
|
|
299
|
+
{ id: 'treatment', name: 'Treatment', description: 'Treatment variant', allocation: 50, isControl: false, config: {} },
|
|
300
|
+
],
|
|
301
|
+
targeting: {},
|
|
302
|
+
metrics: [],
|
|
303
|
+
startDate: new Date(),
|
|
304
|
+
sampleSize: 1000,
|
|
305
|
+
confidenceLevel: 0.95,
|
|
306
|
+
minimumDetectableEffect: 0.05,
|
|
307
|
+
allocations: { control: 50, treatment: 50 },
|
|
308
|
+
stickiness: 'user' as const,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
act(() => {
|
|
312
|
+
result.current.registerExperiment(experimentConfig);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Track exposure first
|
|
316
|
+
await act(async () => {
|
|
317
|
+
await result.current.trackExposure('test-experiment', 'control');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Then track conversion
|
|
321
|
+
await act(async () => {
|
|
322
|
+
await result.current.trackConversion('test-experiment', 'purchase', 99.99);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(mockAmplitudeContext.amplitude.track).toHaveBeenCalledWith(
|
|
326
|
+
'Experiment Converted',
|
|
327
|
+
expect.objectContaining({
|
|
328
|
+
experiment_id: 'test-experiment',
|
|
329
|
+
metric_id: 'purchase',
|
|
330
|
+
conversion_value: 99.99,
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('useSessionReplay hook', () => {
|
|
337
|
+
beforeEach(() => {
|
|
338
|
+
jest.clearAllMocks();
|
|
339
|
+
|
|
340
|
+
// Mock DOM methods
|
|
341
|
+
Object.defineProperty(window, 'MutationObserver', {
|
|
342
|
+
value: jest.fn().mockImplementation(() => ({
|
|
343
|
+
observe: jest.fn(),
|
|
344
|
+
disconnect: jest.fn(),
|
|
345
|
+
})),
|
|
346
|
+
writable: true,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Mock localStorage
|
|
350
|
+
Object.defineProperty(window, 'localStorage', {
|
|
351
|
+
value: {
|
|
352
|
+
getItem: jest.fn(),
|
|
353
|
+
setItem: jest.fn(),
|
|
354
|
+
removeItem: jest.fn(),
|
|
355
|
+
clear: jest.fn(),
|
|
356
|
+
},
|
|
357
|
+
writable: true,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Mock location
|
|
361
|
+
Object.defineProperty(window, 'location', {
|
|
362
|
+
value: {
|
|
363
|
+
href: 'https://example.com/test',
|
|
364
|
+
pathname: '/test',
|
|
365
|
+
},
|
|
366
|
+
writable: true,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Mock document
|
|
370
|
+
Object.defineProperty(document, 'title', {
|
|
371
|
+
value: 'Test Page',
|
|
372
|
+
writable: true,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
Object.defineProperty(document, 'referrer', {
|
|
376
|
+
value: 'https://example.com',
|
|
377
|
+
writable: true,
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should provide session replay functions', () => {
|
|
382
|
+
const { result } = renderHook(() => useSessionReplay());
|
|
383
|
+
|
|
384
|
+
expect(result.current).toHaveProperty('startRecording');
|
|
385
|
+
expect(result.current).toHaveProperty('stopRecording');
|
|
386
|
+
expect(result.current).toHaveProperty('pauseRecording');
|
|
387
|
+
expect(result.current).toHaveProperty('resumeRecording');
|
|
388
|
+
expect(result.current).toHaveProperty('isRecording');
|
|
389
|
+
expect(result.current).toHaveProperty('canRecord');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should start recording when consent is granted', async () => {
|
|
393
|
+
// Mock Math.random to always pass sampling check (default 10% sample rate)
|
|
394
|
+
const originalRandom = Math.random;
|
|
395
|
+
Math.random = jest.fn(() => 0.05); // Less than 0.1 (10% sample rate)
|
|
396
|
+
|
|
397
|
+
const { result } = renderHook(() => useSessionReplay());
|
|
398
|
+
|
|
399
|
+
await act(async () => {
|
|
400
|
+
const started = await result.current.startRecording();
|
|
401
|
+
expect(started).toBe(true);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(result.current.isRecording).toBe(true);
|
|
405
|
+
|
|
406
|
+
// Restore Math.random
|
|
407
|
+
Math.random = originalRandom;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should not start recording without consent', async () => {
|
|
411
|
+
// Temporarily modify the mock context's consent
|
|
412
|
+
const originalConsent = { ...mockAmplitudeContext.consent };
|
|
413
|
+
mockAmplitudeContext.consent = {
|
|
414
|
+
analytics: true,
|
|
415
|
+
sessionReplay: false, // No session replay consent
|
|
416
|
+
experiments: true,
|
|
417
|
+
performance: true,
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const { result } = renderHook(() => useSessionReplay());
|
|
421
|
+
|
|
422
|
+
await act(async () => {
|
|
423
|
+
const started = await result.current.startRecording();
|
|
424
|
+
expect(started).toBe(false);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(result.current.isRecording).toBe(false);
|
|
428
|
+
|
|
429
|
+
// Restore original consent
|
|
430
|
+
mockAmplitudeContext.consent = originalConsent;
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('should stop recording successfully', async () => {
|
|
434
|
+
const { result } = renderHook(() => useSessionReplay());
|
|
435
|
+
|
|
436
|
+
// Start recording first
|
|
437
|
+
await act(async () => {
|
|
438
|
+
await result.current.startRecording();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Then stop it
|
|
442
|
+
await act(async () => {
|
|
443
|
+
await result.current.stopRecording();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
expect(result.current.isRecording).toBe(false);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should pause and resume recording', async () => {
|
|
450
|
+
// Mock Math.random to always pass sampling check (default 10% sample rate)
|
|
451
|
+
const originalRandom = Math.random;
|
|
452
|
+
Math.random = jest.fn(() => 0.05); // Less than 0.1 (10% sample rate)
|
|
453
|
+
|
|
454
|
+
const { result } = renderHook(() => useSessionReplay());
|
|
455
|
+
|
|
456
|
+
// Start recording first
|
|
457
|
+
await act(async () => {
|
|
458
|
+
await result.current.startRecording();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Pause recording
|
|
462
|
+
act(() => {
|
|
463
|
+
result.current.pauseRecording();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
expect(result.current.recordingState.isPaused).toBe(true);
|
|
467
|
+
|
|
468
|
+
// Resume recording
|
|
469
|
+
act(() => {
|
|
470
|
+
result.current.resumeRecording();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(result.current.recordingState.isPaused).toBe(false);
|
|
474
|
+
|
|
475
|
+
// Restore Math.random
|
|
476
|
+
Math.random = originalRandom;
|
|
477
|
+
});
|
|
478
|
+
});
|