react-fathom 0.1.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 (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +199 -0
  3. package/dist/cjs/index.cjs +410 -0
  4. package/dist/cjs/index.cjs.map +1 -0
  5. package/dist/cjs/next/index.cjs +910 -0
  6. package/dist/cjs/next/index.cjs.map +1 -0
  7. package/dist/es/index.js +381 -0
  8. package/dist/es/index.js.map +1 -0
  9. package/dist/es/next/index.js +885 -0
  10. package/dist/es/next/index.js.map +1 -0
  11. package/dist/react-fathom.js +413 -0
  12. package/dist/react-fathom.js.map +1 -0
  13. package/dist/react-fathom.min.js +3 -0
  14. package/dist/react-fathom.min.js.map +1 -0
  15. package/package.json +127 -0
  16. package/src/FathomContext.tsx +5 -0
  17. package/src/FathomProvider.test.tsx +532 -0
  18. package/src/FathomProvider.tsx +122 -0
  19. package/src/components/TrackClick.test.tsx +191 -0
  20. package/src/components/TrackClick.tsx +62 -0
  21. package/src/components/TrackPageview.test.tsx +111 -0
  22. package/src/components/TrackPageview.tsx +36 -0
  23. package/src/components/TrackVisible.test.tsx +311 -0
  24. package/src/components/TrackVisible.tsx +105 -0
  25. package/src/components/index.ts +3 -0
  26. package/src/hooks/index.ts +4 -0
  27. package/src/hooks/useFathom.test.tsx +51 -0
  28. package/src/hooks/useFathom.ts +11 -0
  29. package/src/hooks/useTrackOnClick.test.tsx +197 -0
  30. package/src/hooks/useTrackOnClick.ts +65 -0
  31. package/src/hooks/useTrackOnMount.test.tsx +79 -0
  32. package/src/hooks/useTrackOnMount.ts +24 -0
  33. package/src/hooks/useTrackOnVisible.test.tsx +313 -0
  34. package/src/hooks/useTrackOnVisible.ts +99 -0
  35. package/src/index.ts +4 -0
  36. package/src/next/NextFathomProvider.test.tsx +131 -0
  37. package/src/next/NextFathomProvider.tsx +62 -0
  38. package/src/next/NextFathomProviderApp.test.tsx +308 -0
  39. package/src/next/NextFathomProviderApp.tsx +106 -0
  40. package/src/next/NextFathomProviderPages.test.tsx +330 -0
  41. package/src/next/NextFathomProviderPages.tsx +112 -0
  42. package/src/next/compositions/withAppRouter.test.tsx +113 -0
  43. package/src/next/compositions/withAppRouter.tsx +48 -0
  44. package/src/next/compositions/withPagesRouter.test.tsx +113 -0
  45. package/src/next/compositions/withPagesRouter.tsx +44 -0
  46. package/src/next/index.ts +7 -0
  47. package/src/next/types.ts +19 -0
  48. package/src/types.ts +37 -0
  49. package/types/FathomContext.d.ts +3 -0
  50. package/types/FathomContext.d.ts.map +1 -0
  51. package/types/FathomProvider.d.ts +5 -0
  52. package/types/FathomProvider.d.ts.map +1 -0
  53. package/types/components/TrackClick.d.ts +39 -0
  54. package/types/components/TrackClick.d.ts.map +1 -0
  55. package/types/components/TrackPageview.d.ts +21 -0
  56. package/types/components/TrackPageview.d.ts.map +1 -0
  57. package/types/components/TrackVisible.d.ts +39 -0
  58. package/types/components/TrackVisible.d.ts.map +1 -0
  59. package/types/components/index.d.ts +4 -0
  60. package/types/components/index.d.ts.map +1 -0
  61. package/types/hooks/index.d.ts +5 -0
  62. package/types/hooks/index.d.ts.map +1 -0
  63. package/types/hooks/useFathom.d.ts +6 -0
  64. package/types/hooks/useFathom.d.ts.map +1 -0
  65. package/types/hooks/useTrackOnClick.d.ts +39 -0
  66. package/types/hooks/useTrackOnClick.d.ts.map +1 -0
  67. package/types/hooks/useTrackOnMount.d.ts +14 -0
  68. package/types/hooks/useTrackOnMount.d.ts.map +1 -0
  69. package/types/hooks/useTrackOnVisible.d.ts +43 -0
  70. package/types/hooks/useTrackOnVisible.d.ts.map +1 -0
  71. package/types/index.d.ts +5 -0
  72. package/types/index.d.ts.map +1 -0
  73. package/types/next/AppRouterProvider.d.ts +7 -0
  74. package/types/next/AppRouterProvider.d.ts.map +1 -0
  75. package/types/next/NextFathomProvider.d.ts +34 -0
  76. package/types/next/NextFathomProvider.d.ts.map +1 -0
  77. package/types/next/NextFathomProviderApp.d.ts +6 -0
  78. package/types/next/NextFathomProviderApp.d.ts.map +1 -0
  79. package/types/next/NextFathomProviderPages.d.ts +6 -0
  80. package/types/next/NextFathomProviderPages.d.ts.map +1 -0
  81. package/types/next/PagesRouterProvider.d.ts +7 -0
  82. package/types/next/PagesRouterProvider.d.ts.map +1 -0
  83. package/types/next/compositions/withAppRouter.d.ts +29 -0
  84. package/types/next/compositions/withAppRouter.d.ts.map +1 -0
  85. package/types/next/compositions/withPagesRouter.d.ts +25 -0
  86. package/types/next/compositions/withPagesRouter.d.ts.map +1 -0
  87. package/types/next/index.d.ts +6 -0
  88. package/types/next/index.d.ts.map +1 -0
  89. package/types/next/types.d.ts +16 -0
  90. package/types/next/types.d.ts.map +1 -0
  91. package/types/test-setup.d.ts +2 -0
  92. package/types/test-setup.d.ts.map +1 -0
  93. package/types/types.d.ts +34 -0
  94. package/types/types.d.ts.map +1 -0
  95. package/types/useFathom.d.ts +7 -0
  96. package/types/useFathom.d.ts.map +1 -0
@@ -0,0 +1,532 @@
1
+ import React from 'react'
2
+
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import { renderHook, waitFor } from '@testing-library/react'
6
+
7
+ import { FathomProvider } from './FathomProvider'
8
+ import { useFathom } from './hooks/useFathom'
9
+
10
+ // Mock fathom-client
11
+ vi.mock('fathom-client', () => {
12
+ const mockFathomClient = {
13
+ trackEvent: vi.fn(),
14
+ trackPageview: vi.fn(),
15
+ trackGoal: vi.fn(),
16
+ load: vi.fn(),
17
+ setSite: vi.fn(),
18
+ blockTrackingForMe: vi.fn(),
19
+ enableTrackingForMe: vi.fn(),
20
+ isTrackingEnabled: vi.fn(() => true),
21
+ }
22
+
23
+ return {
24
+ default: mockFathomClient,
25
+ load: mockFathomClient.load,
26
+ }
27
+ })
28
+
29
+ describe('FathomProvider', () => {
30
+ beforeEach(() => {
31
+ vi.clearAllMocks()
32
+ })
33
+
34
+ it('should provide default Fathom client when no client is provided', () => {
35
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
36
+ <FathomProvider>{children}</FathomProvider>
37
+ )
38
+
39
+ const { result } = renderHook(() => useFathom(), { wrapper })
40
+
41
+ expect(result.current.client).toBeDefined()
42
+ expect(result.current.trackEvent).toBeDefined()
43
+ expect(result.current.trackPageview).toBeDefined()
44
+ })
45
+
46
+ it('should use provided client', () => {
47
+ const customClient = {
48
+ trackEvent: vi.fn(),
49
+ trackPageview: vi.fn(),
50
+ trackGoal: vi.fn(),
51
+ load: vi.fn(),
52
+ setSite: vi.fn(),
53
+ blockTrackingForMe: vi.fn(),
54
+ enableTrackingForMe: vi.fn(),
55
+ isTrackingEnabled: vi.fn(() => true),
56
+ }
57
+
58
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
59
+ <FathomProvider client={customClient}>{children}</FathomProvider>
60
+ )
61
+
62
+ const { result } = renderHook(() => useFathom(), { wrapper })
63
+
64
+ expect(result.current.client).toBe(customClient)
65
+ })
66
+
67
+ it('should load Fathom when siteId is provided', async () => {
68
+ const loadSpy = vi.fn()
69
+ const mockClient = {
70
+ trackEvent: vi.fn(),
71
+ trackPageview: vi.fn(),
72
+ trackGoal: vi.fn(),
73
+ load: loadSpy,
74
+ setSite: vi.fn(),
75
+ blockTrackingForMe: vi.fn(),
76
+ enableTrackingForMe: vi.fn(),
77
+ isTrackingEnabled: vi.fn(() => true),
78
+ }
79
+
80
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
81
+ <FathomProvider client={mockClient} siteId="TEST_SITE_ID">
82
+ {children}
83
+ </FathomProvider>
84
+ )
85
+
86
+ renderHook(() => useFathom(), { wrapper })
87
+
88
+ await waitFor(() => {
89
+ expect(loadSpy).toHaveBeenCalledWith('TEST_SITE_ID', undefined)
90
+ })
91
+ })
92
+
93
+ it('should load Fathom with clientOptions when provided', async () => {
94
+ const loadSpy = vi.fn()
95
+ const clientOptions = { honorDNT: true }
96
+ const mockClient = {
97
+ trackEvent: vi.fn(),
98
+ trackPageview: vi.fn(),
99
+ trackGoal: vi.fn(),
100
+ load: loadSpy,
101
+ setSite: vi.fn(),
102
+ blockTrackingForMe: vi.fn(),
103
+ enableTrackingForMe: vi.fn(),
104
+ isTrackingEnabled: vi.fn(() => true),
105
+ }
106
+
107
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
108
+ <FathomProvider
109
+ client={mockClient}
110
+ siteId="TEST_SITE_ID"
111
+ clientOptions={clientOptions}
112
+ >
113
+ {children}
114
+ </FathomProvider>
115
+ )
116
+
117
+ renderHook(() => useFathom(), { wrapper })
118
+
119
+ await waitFor(() => {
120
+ expect(loadSpy).toHaveBeenCalledWith('TEST_SITE_ID', clientOptions)
121
+ })
122
+ })
123
+
124
+ it('should provide trackEvent function', () => {
125
+ const mockClient = {
126
+ trackEvent: vi.fn(),
127
+ trackPageview: vi.fn(),
128
+ trackGoal: vi.fn(),
129
+ load: vi.fn(),
130
+ setSite: vi.fn(),
131
+ blockTrackingForMe: vi.fn(),
132
+ enableTrackingForMe: vi.fn(),
133
+ isTrackingEnabled: vi.fn(() => true),
134
+ }
135
+
136
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
137
+ <FathomProvider client={mockClient}>{children}</FathomProvider>
138
+ )
139
+
140
+ const { result } = renderHook(() => useFathom(), { wrapper })
141
+
142
+ result.current.trackEvent?.('test-event', { id: 'test-id' })
143
+
144
+ expect(mockClient.trackEvent).toHaveBeenCalledWith('test-event', {
145
+ id: 'test-id',
146
+ })
147
+ })
148
+
149
+ it('should merge defaultEventOptions in trackEvent', () => {
150
+ const mockClient = {
151
+ trackEvent: vi.fn(),
152
+ trackPageview: vi.fn(),
153
+ trackGoal: vi.fn(),
154
+ load: vi.fn(),
155
+ setSite: vi.fn(),
156
+ blockTrackingForMe: vi.fn(),
157
+ enableTrackingForMe: vi.fn(),
158
+ isTrackingEnabled: vi.fn(() => true),
159
+ }
160
+
161
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
162
+ <FathomProvider
163
+ client={mockClient}
164
+ defaultEventOptions={{ id: 'default-id' }}
165
+ >
166
+ {children}
167
+ </FathomProvider>
168
+ )
169
+
170
+ const { result } = renderHook(() => useFathom(), { wrapper })
171
+
172
+ result.current.trackEvent?.('test-event', { value: 100 })
173
+
174
+ expect(mockClient.trackEvent).toHaveBeenCalledWith('test-event', {
175
+ id: 'default-id',
176
+ value: 100,
177
+ })
178
+ })
179
+
180
+ it('should override defaultEventOptions with provided options', () => {
181
+ const mockClient = {
182
+ trackEvent: vi.fn(),
183
+ trackPageview: vi.fn(),
184
+ trackGoal: vi.fn(),
185
+ load: vi.fn(),
186
+ setSite: vi.fn(),
187
+ blockTrackingForMe: vi.fn(),
188
+ enableTrackingForMe: vi.fn(),
189
+ isTrackingEnabled: vi.fn(() => true),
190
+ }
191
+
192
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
193
+ <FathomProvider
194
+ client={mockClient}
195
+ defaultEventOptions={{ id: 'default-id' }}
196
+ >
197
+ {children}
198
+ </FathomProvider>
199
+ )
200
+
201
+ const { result } = renderHook(() => useFathom(), { wrapper })
202
+
203
+ result.current.trackEvent?.('test-event', { id: 'override-id' })
204
+
205
+ expect(mockClient.trackEvent).toHaveBeenCalledWith('test-event', {
206
+ id: 'override-id',
207
+ })
208
+ })
209
+
210
+ it('should provide trackPageview function', () => {
211
+ const mockClient = {
212
+ trackEvent: vi.fn(),
213
+ trackPageview: vi.fn(),
214
+ trackGoal: vi.fn(),
215
+ load: vi.fn(),
216
+ setSite: vi.fn(),
217
+ blockTrackingForMe: vi.fn(),
218
+ enableTrackingForMe: vi.fn(),
219
+ isTrackingEnabled: vi.fn(() => true),
220
+ }
221
+
222
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
223
+ <FathomProvider client={mockClient}>{children}</FathomProvider>
224
+ )
225
+
226
+ const { result } = renderHook(() => useFathom(), { wrapper })
227
+
228
+ result.current.trackPageview?.({ url: '/test-page' })
229
+
230
+ expect(mockClient.trackPageview).toHaveBeenCalledWith({ url: '/test-page' })
231
+ })
232
+
233
+ it('should merge defaultPageviewOptions in trackPageview', () => {
234
+ const mockClient = {
235
+ trackEvent: vi.fn(),
236
+ trackPageview: vi.fn(),
237
+ trackGoal: vi.fn(),
238
+ load: vi.fn(),
239
+ setSite: vi.fn(),
240
+ blockTrackingForMe: vi.fn(),
241
+ enableTrackingForMe: vi.fn(),
242
+ isTrackingEnabled: vi.fn(() => true),
243
+ }
244
+
245
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
246
+ <FathomProvider
247
+ client={mockClient}
248
+ defaultPageviewOptions={{ url: '/default' }}
249
+ >
250
+ {children}
251
+ </FathomProvider>
252
+ )
253
+
254
+ const { result } = renderHook(() => useFathom(), { wrapper })
255
+
256
+ result.current.trackPageview?.({ referrer: 'https://example.com' })
257
+
258
+ expect(mockClient.trackPageview).toHaveBeenCalledWith({
259
+ url: '/default',
260
+ referrer: 'https://example.com',
261
+ })
262
+ })
263
+
264
+ it('should override defaultPageviewOptions with provided options', () => {
265
+ const mockClient = {
266
+ trackEvent: vi.fn(),
267
+ trackPageview: vi.fn(),
268
+ trackGoal: vi.fn(),
269
+ load: vi.fn(),
270
+ setSite: vi.fn(),
271
+ blockTrackingForMe: vi.fn(),
272
+ enableTrackingForMe: vi.fn(),
273
+ isTrackingEnabled: vi.fn(() => true),
274
+ }
275
+
276
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
277
+ <FathomProvider
278
+ client={mockClient}
279
+ defaultPageviewOptions={{ url: '/default' }}
280
+ >
281
+ {children}
282
+ </FathomProvider>
283
+ )
284
+
285
+ const { result } = renderHook(() => useFathom(), { wrapper })
286
+
287
+ result.current.trackPageview?.({ url: '/override' })
288
+
289
+ expect(mockClient.trackPageview).toHaveBeenCalledWith({ url: '/override' })
290
+ })
291
+
292
+ it('should provide trackGoal function', () => {
293
+ const mockClient = {
294
+ trackEvent: vi.fn(),
295
+ trackPageview: vi.fn(),
296
+ trackGoal: vi.fn(),
297
+ load: vi.fn(),
298
+ setSite: vi.fn(),
299
+ blockTrackingForMe: vi.fn(),
300
+ enableTrackingForMe: vi.fn(),
301
+ isTrackingEnabled: vi.fn(() => true),
302
+ }
303
+
304
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
305
+ <FathomProvider client={mockClient}>{children}</FathomProvider>
306
+ )
307
+
308
+ const { result } = renderHook(() => useFathom(), { wrapper })
309
+
310
+ result.current.trackGoal?.('GOAL_CODE', 1000)
311
+
312
+ expect(mockClient.trackGoal).toHaveBeenCalledWith('GOAL_CODE', 1000)
313
+ })
314
+
315
+ it('should provide other Fathom methods', () => {
316
+ const loadSpy = vi.fn()
317
+ const setSiteSpy = vi.fn()
318
+ const blockTrackingSpy = vi.fn()
319
+ const enableTrackingSpy = vi.fn()
320
+ const isTrackingEnabledSpy = vi.fn(() => true)
321
+
322
+ const mockClient = {
323
+ trackEvent: vi.fn(),
324
+ trackPageview: vi.fn(),
325
+ trackGoal: vi.fn(),
326
+ load: loadSpy,
327
+ setSite: setSiteSpy,
328
+ blockTrackingForMe: blockTrackingSpy,
329
+ enableTrackingForMe: enableTrackingSpy,
330
+ isTrackingEnabled: isTrackingEnabledSpy,
331
+ }
332
+
333
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
334
+ <FathomProvider client={mockClient}>{children}</FathomProvider>
335
+ )
336
+
337
+ const { result } = renderHook(() => useFathom(), { wrapper })
338
+
339
+ result.current.load?.('SITE_ID')
340
+ result.current.setSite?.('SITE_ID')
341
+ result.current.blockTrackingForMe?.()
342
+ result.current.enableTrackingForMe?.()
343
+ result.current.isTrackingEnabled?.()
344
+
345
+ expect(loadSpy).toHaveBeenCalledWith('SITE_ID', undefined)
346
+ expect(setSiteSpy).toHaveBeenCalledWith('SITE_ID')
347
+ expect(blockTrackingSpy).toHaveBeenCalled()
348
+ expect(enableTrackingSpy).toHaveBeenCalled()
349
+ expect(isTrackingEnabledSpy).toHaveBeenCalled()
350
+ })
351
+
352
+ it('should compose nested providers - child uses parent client', () => {
353
+ const parentClient = {
354
+ trackEvent: vi.fn(),
355
+ trackPageview: vi.fn(),
356
+ trackGoal: vi.fn(),
357
+ load: vi.fn(),
358
+ setSite: vi.fn(),
359
+ blockTrackingForMe: vi.fn(),
360
+ enableTrackingForMe: vi.fn(),
361
+ isTrackingEnabled: vi.fn(() => true),
362
+ }
363
+
364
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
365
+ <FathomProvider client={parentClient}>
366
+ <FathomProvider>{children}</FathomProvider>
367
+ </FathomProvider>
368
+ )
369
+
370
+ const { result } = renderHook(() => useFathom(), { wrapper })
371
+
372
+ expect(result.current.client).toBe(parentClient)
373
+ })
374
+
375
+ it('should compose nested providers - child overrides parent client', () => {
376
+ const parentClient = {
377
+ trackEvent: vi.fn(),
378
+ trackPageview: vi.fn(),
379
+ trackGoal: vi.fn(),
380
+ load: vi.fn(),
381
+ setSite: vi.fn(),
382
+ blockTrackingForMe: vi.fn(),
383
+ enableTrackingForMe: vi.fn(),
384
+ isTrackingEnabled: vi.fn(() => true),
385
+ }
386
+
387
+ const childClient = {
388
+ trackEvent: vi.fn(),
389
+ trackPageview: vi.fn(),
390
+ trackGoal: vi.fn(),
391
+ load: vi.fn(),
392
+ setSite: vi.fn(),
393
+ blockTrackingForMe: vi.fn(),
394
+ enableTrackingForMe: vi.fn(),
395
+ isTrackingEnabled: vi.fn(() => true),
396
+ }
397
+
398
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
399
+ <FathomProvider client={parentClient}>
400
+ <FathomProvider client={childClient}>{children}</FathomProvider>
401
+ </FathomProvider>
402
+ )
403
+
404
+ const { result } = renderHook(() => useFathom(), { wrapper })
405
+
406
+ expect(result.current.client).toBe(childClient)
407
+ })
408
+
409
+ it('should compose nested providers - child inherits parent defaultPageviewOptions', () => {
410
+ const mockClient = {
411
+ trackEvent: vi.fn(),
412
+ trackPageview: vi.fn(),
413
+ trackGoal: vi.fn(),
414
+ load: vi.fn(),
415
+ setSite: vi.fn(),
416
+ blockTrackingForMe: vi.fn(),
417
+ enableTrackingForMe: vi.fn(),
418
+ isTrackingEnabled: vi.fn(() => true),
419
+ }
420
+
421
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
422
+ <FathomProvider
423
+ client={mockClient}
424
+ defaultPageviewOptions={{ url: '/parent' }}
425
+ >
426
+ <FathomProvider>{children}</FathomProvider>
427
+ </FathomProvider>
428
+ )
429
+
430
+ const { result } = renderHook(() => useFathom(), { wrapper })
431
+
432
+ result.current.trackPageview?.()
433
+
434
+ expect(mockClient.trackPageview).toHaveBeenCalledWith({ url: '/parent' })
435
+ })
436
+
437
+ it('should compose nested providers - child overrides parent defaultPageviewOptions', () => {
438
+ const mockClient = {
439
+ trackEvent: vi.fn(),
440
+ trackPageview: vi.fn(),
441
+ trackGoal: vi.fn(),
442
+ load: vi.fn(),
443
+ setSite: vi.fn(),
444
+ blockTrackingForMe: vi.fn(),
445
+ enableTrackingForMe: vi.fn(),
446
+ isTrackingEnabled: vi.fn(() => true),
447
+ }
448
+
449
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
450
+ <FathomProvider
451
+ client={mockClient}
452
+ defaultPageviewOptions={{ url: '/parent' }}
453
+ >
454
+ <FathomProvider defaultPageviewOptions={{ url: '/child' }}>
455
+ {children}
456
+ </FathomProvider>
457
+ </FathomProvider>
458
+ )
459
+
460
+ const { result } = renderHook(() => useFathom(), { wrapper })
461
+
462
+ result.current.trackPageview?.()
463
+
464
+ expect(mockClient.trackPageview).toHaveBeenCalledWith({ url: '/child' })
465
+ })
466
+
467
+ it('should compose nested providers - child inherits parent defaultEventOptions', () => {
468
+ const mockClient = {
469
+ trackEvent: vi.fn(),
470
+ trackPageview: vi.fn(),
471
+ trackGoal: vi.fn(),
472
+ load: vi.fn(),
473
+ setSite: vi.fn(),
474
+ blockTrackingForMe: vi.fn(),
475
+ enableTrackingForMe: vi.fn(),
476
+ isTrackingEnabled: vi.fn(() => true),
477
+ }
478
+
479
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
480
+ <FathomProvider
481
+ client={mockClient}
482
+ defaultEventOptions={{ id: 'parent-id' }}
483
+ >
484
+ <FathomProvider>{children}</FathomProvider>
485
+ </FathomProvider>
486
+ )
487
+
488
+ const { result } = renderHook(() => useFathom(), { wrapper })
489
+
490
+ result.current.trackEvent?.('test-event')
491
+
492
+ expect(mockClient.trackEvent).toHaveBeenCalledWith('test-event', {
493
+ id: 'parent-id',
494
+ })
495
+ })
496
+
497
+ it('should compose nested providers - child overrides parent defaultEventOptions', () => {
498
+ const mockClient = {
499
+ trackEvent: vi.fn(),
500
+ trackPageview: vi.fn(),
501
+ trackGoal: vi.fn(),
502
+ load: vi.fn(),
503
+ setSite: vi.fn(),
504
+ blockTrackingForMe: vi.fn(),
505
+ enableTrackingForMe: vi.fn(),
506
+ isTrackingEnabled: vi.fn(() => true),
507
+ }
508
+
509
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
510
+ <FathomProvider
511
+ client={mockClient}
512
+ defaultEventOptions={{ id: 'parent-id' }}
513
+ >
514
+ <FathomProvider defaultEventOptions={{ id: 'child-id' }}>
515
+ {children}
516
+ </FathomProvider>
517
+ </FathomProvider>
518
+ )
519
+
520
+ const { result } = renderHook(() => useFathom(), { wrapper })
521
+
522
+ result.current.trackEvent?.('test-event')
523
+
524
+ expect(mockClient.trackEvent).toHaveBeenCalledWith('test-event', {
525
+ id: 'child-id',
526
+ })
527
+ })
528
+
529
+ it('should have displayName', () => {
530
+ expect(FathomProvider.displayName).toBe('FathomProvider')
531
+ })
532
+ })
@@ -0,0 +1,122 @@
1
+ import React, { useCallback, useContext, useEffect, useMemo } from 'react'
2
+
3
+ import * as Fathom from 'fathom-client'
4
+ import type { EventOptions, LoadOptions, PageViewOptions } from 'fathom-client'
5
+
6
+ import { FathomContext } from './FathomContext'
7
+ import type { FathomProviderProps } from './types'
8
+
9
+ const FathomProvider: React.FC<FathomProviderProps> = ({
10
+ children,
11
+ client: providedClient,
12
+ clientOptions,
13
+ disableDefaultTrack,
14
+ siteId,
15
+ defaultPageviewOptions: providedDefaultPageviewOptions,
16
+ defaultEventOptions: providedDefaultEventOptions,
17
+ }) => {
18
+ // Read parent context if it exists
19
+ const parentContext = useContext(FathomContext)
20
+
21
+ // Use provided client or fall back to parent client or default Fathom
22
+ const client = useMemo(
23
+ () => providedClient ?? parentContext.client ?? Fathom,
24
+ [providedClient, parentContext.client],
25
+ )
26
+
27
+ // Merge defaultPageviewOptions: provided > parent > undefined
28
+ const defaultPageviewOptions = useMemo(
29
+ () =>
30
+ providedDefaultPageviewOptions ?? parentContext.defaultPageviewOptions,
31
+ [providedDefaultPageviewOptions, parentContext.defaultPageviewOptions],
32
+ )
33
+
34
+ // Merge defaultEventOptions: provided > parent > undefined
35
+ const defaultEventOptions = useMemo(
36
+ () => providedDefaultEventOptions ?? parentContext.defaultEventOptions,
37
+ [providedDefaultEventOptions, parentContext.defaultEventOptions],
38
+ )
39
+
40
+ const blockTrackingForMe = useCallback(() => {
41
+ client.blockTrackingForMe()
42
+ }, [client])
43
+
44
+ const enableTrackingForMe = useCallback(() => {
45
+ client.enableTrackingForMe()
46
+ }, [client])
47
+
48
+ const isTrackingEnabled = useCallback(() => {
49
+ return client.isTrackingEnabled() ?? false
50
+ }, [client])
51
+
52
+ const load = useCallback(
53
+ (siteId: string, clientOptions?: LoadOptions) => {
54
+ client.load(siteId, clientOptions)
55
+ },
56
+ [client],
57
+ )
58
+
59
+ const setSite = useCallback(
60
+ (siteId: string) => {
61
+ client.setSite(siteId)
62
+ },
63
+ [client],
64
+ )
65
+
66
+ const trackEvent = useCallback(
67
+ (category: string, options?: EventOptions) => {
68
+ client.trackEvent(category, {
69
+ ...defaultEventOptions,
70
+ ...options,
71
+ })
72
+ },
73
+ [client, defaultEventOptions],
74
+ )
75
+
76
+ const trackPageview = useCallback(
77
+ (options?: PageViewOptions) => {
78
+ client.trackPageview({
79
+ ...defaultPageviewOptions,
80
+ ...options,
81
+ })
82
+ },
83
+ [client, defaultPageviewOptions],
84
+ )
85
+
86
+ const trackGoal = useCallback(
87
+ (code: string, cents: number) => {
88
+ client.trackGoal(code, cents)
89
+ },
90
+ [client],
91
+ )
92
+
93
+ useEffect(() => {
94
+ if (siteId !== undefined) {
95
+ load(siteId, clientOptions)
96
+ }
97
+ }, [clientOptions, load, siteId])
98
+
99
+ return (
100
+ <FathomContext.Provider
101
+ value={{
102
+ blockTrackingForMe,
103
+ enableTrackingForMe,
104
+ isTrackingEnabled,
105
+ load,
106
+ setSite,
107
+ trackEvent,
108
+ trackGoal,
109
+ trackPageview,
110
+ client,
111
+ defaultPageviewOptions,
112
+ defaultEventOptions,
113
+ }}
114
+ >
115
+ {children}
116
+ </FathomContext.Provider>
117
+ )
118
+ }
119
+
120
+ FathomProvider.displayName = 'FathomProvider'
121
+
122
+ export { FathomProvider }