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.
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/cjs/index.cjs +410 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/next/index.cjs +910 -0
- package/dist/cjs/next/index.cjs.map +1 -0
- package/dist/es/index.js +381 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/next/index.js +885 -0
- package/dist/es/next/index.js.map +1 -0
- package/dist/react-fathom.js +413 -0
- package/dist/react-fathom.js.map +1 -0
- package/dist/react-fathom.min.js +3 -0
- package/dist/react-fathom.min.js.map +1 -0
- package/package.json +127 -0
- package/src/FathomContext.tsx +5 -0
- package/src/FathomProvider.test.tsx +532 -0
- package/src/FathomProvider.tsx +122 -0
- package/src/components/TrackClick.test.tsx +191 -0
- package/src/components/TrackClick.tsx +62 -0
- package/src/components/TrackPageview.test.tsx +111 -0
- package/src/components/TrackPageview.tsx +36 -0
- package/src/components/TrackVisible.test.tsx +311 -0
- package/src/components/TrackVisible.tsx +105 -0
- package/src/components/index.ts +3 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useFathom.test.tsx +51 -0
- package/src/hooks/useFathom.ts +11 -0
- package/src/hooks/useTrackOnClick.test.tsx +197 -0
- package/src/hooks/useTrackOnClick.ts +65 -0
- package/src/hooks/useTrackOnMount.test.tsx +79 -0
- package/src/hooks/useTrackOnMount.ts +24 -0
- package/src/hooks/useTrackOnVisible.test.tsx +313 -0
- package/src/hooks/useTrackOnVisible.ts +99 -0
- package/src/index.ts +4 -0
- package/src/next/NextFathomProvider.test.tsx +131 -0
- package/src/next/NextFathomProvider.tsx +62 -0
- package/src/next/NextFathomProviderApp.test.tsx +308 -0
- package/src/next/NextFathomProviderApp.tsx +106 -0
- package/src/next/NextFathomProviderPages.test.tsx +330 -0
- package/src/next/NextFathomProviderPages.tsx +112 -0
- package/src/next/compositions/withAppRouter.test.tsx +113 -0
- package/src/next/compositions/withAppRouter.tsx +48 -0
- package/src/next/compositions/withPagesRouter.test.tsx +113 -0
- package/src/next/compositions/withPagesRouter.tsx +44 -0
- package/src/next/index.ts +7 -0
- package/src/next/types.ts +19 -0
- package/src/types.ts +37 -0
- package/types/FathomContext.d.ts +3 -0
- package/types/FathomContext.d.ts.map +1 -0
- package/types/FathomProvider.d.ts +5 -0
- package/types/FathomProvider.d.ts.map +1 -0
- package/types/components/TrackClick.d.ts +39 -0
- package/types/components/TrackClick.d.ts.map +1 -0
- package/types/components/TrackPageview.d.ts +21 -0
- package/types/components/TrackPageview.d.ts.map +1 -0
- package/types/components/TrackVisible.d.ts +39 -0
- package/types/components/TrackVisible.d.ts.map +1 -0
- package/types/components/index.d.ts +4 -0
- package/types/components/index.d.ts.map +1 -0
- package/types/hooks/index.d.ts +5 -0
- package/types/hooks/index.d.ts.map +1 -0
- package/types/hooks/useFathom.d.ts +6 -0
- package/types/hooks/useFathom.d.ts.map +1 -0
- package/types/hooks/useTrackOnClick.d.ts +39 -0
- package/types/hooks/useTrackOnClick.d.ts.map +1 -0
- package/types/hooks/useTrackOnMount.d.ts +14 -0
- package/types/hooks/useTrackOnMount.d.ts.map +1 -0
- package/types/hooks/useTrackOnVisible.d.ts +43 -0
- package/types/hooks/useTrackOnVisible.d.ts.map +1 -0
- package/types/index.d.ts +5 -0
- package/types/index.d.ts.map +1 -0
- package/types/next/AppRouterProvider.d.ts +7 -0
- package/types/next/AppRouterProvider.d.ts.map +1 -0
- package/types/next/NextFathomProvider.d.ts +34 -0
- package/types/next/NextFathomProvider.d.ts.map +1 -0
- package/types/next/NextFathomProviderApp.d.ts +6 -0
- package/types/next/NextFathomProviderApp.d.ts.map +1 -0
- package/types/next/NextFathomProviderPages.d.ts +6 -0
- package/types/next/NextFathomProviderPages.d.ts.map +1 -0
- package/types/next/PagesRouterProvider.d.ts +7 -0
- package/types/next/PagesRouterProvider.d.ts.map +1 -0
- package/types/next/compositions/withAppRouter.d.ts +29 -0
- package/types/next/compositions/withAppRouter.d.ts.map +1 -0
- package/types/next/compositions/withPagesRouter.d.ts +25 -0
- package/types/next/compositions/withPagesRouter.d.ts.map +1 -0
- package/types/next/index.d.ts +6 -0
- package/types/next/index.d.ts.map +1 -0
- package/types/next/types.d.ts +16 -0
- package/types/next/types.d.ts.map +1 -0
- package/types/test-setup.d.ts +2 -0
- package/types/test-setup.d.ts.map +1 -0
- package/types/types.d.ts +34 -0
- package/types/types.d.ts.map +1 -0
- package/types/useFathom.d.ts +7 -0
- package/types/useFathom.d.ts.map +1 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { render, renderHook, waitFor } from '@testing-library/react'
|
|
6
|
+
|
|
7
|
+
import { FathomProvider } from '../FathomProvider'
|
|
8
|
+
import { useTrackOnVisible } from './useTrackOnVisible'
|
|
9
|
+
|
|
10
|
+
describe('useTrackOnVisible', () => {
|
|
11
|
+
const mockTrackEvent = vi.fn()
|
|
12
|
+
const mockClient = {
|
|
13
|
+
trackEvent: mockTrackEvent,
|
|
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
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
24
|
+
<FathomProvider client={mockClient}>{children}</FathomProvider>
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
let mockObserve: ReturnType<typeof vi.fn>
|
|
28
|
+
let mockDisconnect: ReturnType<typeof vi.fn>
|
|
29
|
+
let observerCallback: (entries: IntersectionObserverEntry[]) => void
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks()
|
|
33
|
+
mockObserve = vi.fn()
|
|
34
|
+
mockDisconnect = vi.fn()
|
|
35
|
+
|
|
36
|
+
global.IntersectionObserver = class MockIntersectionObserver {
|
|
37
|
+
observe = mockObserve
|
|
38
|
+
disconnect = mockDisconnect
|
|
39
|
+
unobserve = vi.fn()
|
|
40
|
+
|
|
41
|
+
constructor(callback: (entries: IntersectionObserverEntry[]) => void) {
|
|
42
|
+
observerCallback = callback
|
|
43
|
+
}
|
|
44
|
+
} as any
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
delete (global as any).IntersectionObserver
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should return a ref object', () => {
|
|
52
|
+
const { result } = renderHook(
|
|
53
|
+
() => useTrackOnVisible({ eventName: 'test-event' }),
|
|
54
|
+
{ wrapper },
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
expect(result.current).toHaveProperty('current')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should create IntersectionObserver when ref is attached', async () => {
|
|
61
|
+
const TestComponent = () => {
|
|
62
|
+
const ref = useTrackOnVisible({ eventName: 'test-event' })
|
|
63
|
+
return <div ref={ref}>Test</div>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
render(
|
|
67
|
+
<FathomProvider client={mockClient}>
|
|
68
|
+
<TestComponent />
|
|
69
|
+
</FathomProvider>,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
await waitFor(() => {
|
|
73
|
+
expect(mockObserve).toHaveBeenCalled()
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should track event when element becomes visible', async () => {
|
|
78
|
+
const TestComponent = () => {
|
|
79
|
+
const ref = useTrackOnVisible({ eventName: 'test-event' })
|
|
80
|
+
return <div ref={ref}>Test</div>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
render(
|
|
84
|
+
<FathomProvider client={mockClient}>
|
|
85
|
+
<TestComponent />
|
|
86
|
+
</FathomProvider>,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
// Wait for observer to be created
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(mockObserve).toHaveBeenCalled()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Simulate intersection
|
|
95
|
+
const element = document.querySelector('div')
|
|
96
|
+
const mockEntry = {
|
|
97
|
+
isIntersecting: true,
|
|
98
|
+
target: element!,
|
|
99
|
+
} as IntersectionObserverEntry
|
|
100
|
+
|
|
101
|
+
observerCallback([mockEntry])
|
|
102
|
+
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(mockTrackEvent).toHaveBeenCalledTimes(1)
|
|
105
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('test-event', {})
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should track event with options', async () => {
|
|
110
|
+
const TestComponent = () => {
|
|
111
|
+
const ref = useTrackOnVisible({
|
|
112
|
+
eventName: 'test-event',
|
|
113
|
+
id: 'test-id',
|
|
114
|
+
value: 100,
|
|
115
|
+
})
|
|
116
|
+
return <div ref={ref}>Test</div>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
render(
|
|
120
|
+
<FathomProvider client={mockClient}>
|
|
121
|
+
<TestComponent />
|
|
122
|
+
</FathomProvider>,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// Wait for observer to be created
|
|
126
|
+
await waitFor(() => {
|
|
127
|
+
expect(mockObserve).toHaveBeenCalled()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const element = document.querySelector('div')
|
|
131
|
+
const mockEntry = {
|
|
132
|
+
isIntersecting: true,
|
|
133
|
+
target: element!,
|
|
134
|
+
} as IntersectionObserverEntry
|
|
135
|
+
|
|
136
|
+
observerCallback([mockEntry])
|
|
137
|
+
|
|
138
|
+
await waitFor(() => {
|
|
139
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('test-event', {
|
|
140
|
+
id: 'test-id',
|
|
141
|
+
value: 100,
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should track only once when trackOnce is true', async () => {
|
|
147
|
+
const TestComponent = () => {
|
|
148
|
+
const ref = useTrackOnVisible({
|
|
149
|
+
eventName: 'test-event',
|
|
150
|
+
trackOnce: true,
|
|
151
|
+
})
|
|
152
|
+
return <div ref={ref}>Test</div>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
render(
|
|
156
|
+
<FathomProvider client={mockClient}>
|
|
157
|
+
<TestComponent />
|
|
158
|
+
</FathomProvider>,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
// Wait for observer to be created
|
|
162
|
+
await waitFor(() => {
|
|
163
|
+
expect(mockObserve).toHaveBeenCalled()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const element = document.querySelector('div')
|
|
167
|
+
const mockEntry = {
|
|
168
|
+
isIntersecting: true,
|
|
169
|
+
target: element!,
|
|
170
|
+
} as IntersectionObserverEntry
|
|
171
|
+
|
|
172
|
+
observerCallback([mockEntry])
|
|
173
|
+
observerCallback([mockEntry])
|
|
174
|
+
observerCallback([mockEntry])
|
|
175
|
+
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(mockTrackEvent).toHaveBeenCalledTimes(1)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should track multiple times when trackOnce is false', async () => {
|
|
182
|
+
const TestComponent = () => {
|
|
183
|
+
const ref = useTrackOnVisible({
|
|
184
|
+
eventName: 'test-event',
|
|
185
|
+
trackOnce: false,
|
|
186
|
+
})
|
|
187
|
+
return <div ref={ref}>Test</div>
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
render(
|
|
191
|
+
<FathomProvider client={mockClient}>
|
|
192
|
+
<TestComponent />
|
|
193
|
+
</FathomProvider>,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
// Wait for observer to be created
|
|
197
|
+
await waitFor(() => {
|
|
198
|
+
expect(mockObserve).toHaveBeenCalled()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const element = document.querySelector('div')
|
|
202
|
+
const mockEntry = {
|
|
203
|
+
isIntersecting: true,
|
|
204
|
+
target: element!,
|
|
205
|
+
} as IntersectionObserverEntry
|
|
206
|
+
|
|
207
|
+
observerCallback([mockEntry])
|
|
208
|
+
observerCallback([mockEntry])
|
|
209
|
+
observerCallback([mockEntry])
|
|
210
|
+
|
|
211
|
+
await waitFor(() => {
|
|
212
|
+
expect(mockTrackEvent).toHaveBeenCalledTimes(3)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should call callback when element becomes visible', async () => {
|
|
217
|
+
const callback = vi.fn()
|
|
218
|
+
|
|
219
|
+
const TestComponent = () => {
|
|
220
|
+
const ref = useTrackOnVisible({
|
|
221
|
+
eventName: 'test-event',
|
|
222
|
+
callback,
|
|
223
|
+
})
|
|
224
|
+
return <div ref={ref}>Test</div>
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
render(
|
|
228
|
+
<FathomProvider client={mockClient}>
|
|
229
|
+
<TestComponent />
|
|
230
|
+
</FathomProvider>,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
// Wait for observer to be created
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
expect(mockObserve).toHaveBeenCalled()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const element = document.querySelector('div')
|
|
239
|
+
const mockEntry = {
|
|
240
|
+
isIntersecting: true,
|
|
241
|
+
target: element!,
|
|
242
|
+
} as IntersectionObserverEntry
|
|
243
|
+
|
|
244
|
+
observerCallback([mockEntry])
|
|
245
|
+
|
|
246
|
+
await waitFor(() => {
|
|
247
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
248
|
+
expect(callback).toHaveBeenCalledWith(mockEntry)
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('should use custom observer options', async () => {
|
|
253
|
+
const observerOptions = { threshold: 0.5 }
|
|
254
|
+
let capturedOptions: IntersectionObserverInit | undefined
|
|
255
|
+
|
|
256
|
+
const OriginalObserver = global.IntersectionObserver
|
|
257
|
+
global.IntersectionObserver = class MockIntersectionObserver {
|
|
258
|
+
observe = mockObserve
|
|
259
|
+
disconnect = mockDisconnect
|
|
260
|
+
unobserve = vi.fn()
|
|
261
|
+
|
|
262
|
+
constructor(
|
|
263
|
+
callback: (entries: IntersectionObserverEntry[]) => void,
|
|
264
|
+
options?: IntersectionObserverInit,
|
|
265
|
+
) {
|
|
266
|
+
capturedOptions = options
|
|
267
|
+
observerCallback = callback
|
|
268
|
+
}
|
|
269
|
+
} as any
|
|
270
|
+
|
|
271
|
+
const TestComponent = () => {
|
|
272
|
+
const ref = useTrackOnVisible({
|
|
273
|
+
eventName: 'test-event',
|
|
274
|
+
observerOptions,
|
|
275
|
+
})
|
|
276
|
+
return <div ref={ref}>Test</div>
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
render(
|
|
280
|
+
<FathomProvider client={mockClient}>
|
|
281
|
+
<TestComponent />
|
|
282
|
+
</FathomProvider>,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
await waitFor(() => {
|
|
286
|
+
expect(capturedOptions?.threshold).toBe(0.5)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
global.IntersectionObserver = OriginalObserver
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should disconnect observer on unmount', async () => {
|
|
293
|
+
const TestComponent = () => {
|
|
294
|
+
const ref = useTrackOnVisible({ eventName: 'test-event' })
|
|
295
|
+
return <div ref={ref}>Test</div>
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { unmount } = render(
|
|
299
|
+
<FathomProvider client={mockClient}>
|
|
300
|
+
<TestComponent />
|
|
301
|
+
</FathomProvider>,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
// Wait for observer to be created
|
|
305
|
+
await waitFor(() => {
|
|
306
|
+
expect(mockObserve).toHaveBeenCalled()
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
unmount()
|
|
310
|
+
|
|
311
|
+
expect(mockDisconnect).toHaveBeenCalledTimes(1)
|
|
312
|
+
})
|
|
313
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import type { RefObject } from 'react'
|
|
3
|
+
|
|
4
|
+
import type { EventOptions } from 'fathom-client'
|
|
5
|
+
|
|
6
|
+
import { useFathom } from './useFathom'
|
|
7
|
+
|
|
8
|
+
export interface UseTrackOnVisibleOptions extends EventOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Event name to track
|
|
11
|
+
*/
|
|
12
|
+
eventName: string
|
|
13
|
+
/**
|
|
14
|
+
* Intersection observer options
|
|
15
|
+
*/
|
|
16
|
+
observerOptions?: IntersectionObserverInit
|
|
17
|
+
/**
|
|
18
|
+
* Whether to track only once or every time it becomes visible
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
trackOnce?: boolean
|
|
22
|
+
/**
|
|
23
|
+
* Optional callback function to run after tracking
|
|
24
|
+
* Receives the intersection observer entry as a parameter
|
|
25
|
+
*/
|
|
26
|
+
callback?: (entry: IntersectionObserverEntry) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Hook to track an event when an element becomes visible (using Intersection Observer)
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* function Section() {
|
|
35
|
+
* const ref = useTrackOnVisible({
|
|
36
|
+
* eventName: 'section-viewed',
|
|
37
|
+
* section: 'hero',
|
|
38
|
+
* callback: (entry) => {
|
|
39
|
+
* console.log('Section is visible!', entry.isIntersecting)
|
|
40
|
+
* // Your custom logic here
|
|
41
|
+
* },
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* return <section ref={ref}>Content</section>
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export const useTrackOnVisible = (
|
|
49
|
+
options: UseTrackOnVisibleOptions,
|
|
50
|
+
): RefObject<HTMLElement | null> => {
|
|
51
|
+
const { trackEvent } = useFathom()
|
|
52
|
+
const {
|
|
53
|
+
eventName,
|
|
54
|
+
observerOptions,
|
|
55
|
+
trackOnce = true,
|
|
56
|
+
callback,
|
|
57
|
+
...eventOptions
|
|
58
|
+
} = options
|
|
59
|
+
const ref = useRef<HTMLElement | null>(null)
|
|
60
|
+
const hasTracked = useRef(false)
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const element = ref.current
|
|
64
|
+
if (!element) return
|
|
65
|
+
|
|
66
|
+
const observer = new IntersectionObserver(
|
|
67
|
+
(entries) => {
|
|
68
|
+
entries.forEach((entry) => {
|
|
69
|
+
if (entry.isIntersecting) {
|
|
70
|
+
if (!trackOnce || !hasTracked.current) {
|
|
71
|
+
trackEvent?.(eventName, eventOptions)
|
|
72
|
+
callback?.(entry)
|
|
73
|
+
hasTracked.current = true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
threshold: 0.1,
|
|
80
|
+
...observerOptions,
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
observer.observe(element)
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
observer.disconnect()
|
|
88
|
+
}
|
|
89
|
+
}, [
|
|
90
|
+
eventName,
|
|
91
|
+
eventOptions,
|
|
92
|
+
observerOptions,
|
|
93
|
+
trackOnce,
|
|
94
|
+
trackEvent,
|
|
95
|
+
callback,
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
return ref
|
|
99
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
6
|
+
|
|
7
|
+
import { NextFathomProvider } from './NextFathomProvider'
|
|
8
|
+
|
|
9
|
+
// Mock the lazy-loaded providers
|
|
10
|
+
vi.mock('./NextFathomProviderApp', () => ({
|
|
11
|
+
default: ({
|
|
12
|
+
children,
|
|
13
|
+
siteId,
|
|
14
|
+
}: {
|
|
15
|
+
children: React.ReactNode
|
|
16
|
+
siteId?: string
|
|
17
|
+
}) => (
|
|
18
|
+
<div data-testid="app-provider" data-site-id={siteId}>
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
vi.mock('./NextFathomProviderPages', () => ({
|
|
25
|
+
default: ({
|
|
26
|
+
children,
|
|
27
|
+
siteId,
|
|
28
|
+
}: {
|
|
29
|
+
children: React.ReactNode
|
|
30
|
+
siteId?: string
|
|
31
|
+
}) => (
|
|
32
|
+
<div data-testid="pages-provider" data-site-id={siteId}>
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
),
|
|
36
|
+
}))
|
|
37
|
+
|
|
38
|
+
describe('NextFathomProvider', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.clearAllMocks()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should render App Router provider by default', async () => {
|
|
44
|
+
render(
|
|
45
|
+
<NextFathomProvider siteId="TEST_SITE_ID">
|
|
46
|
+
<div>Test Content</div>
|
|
47
|
+
</NextFathomProvider>,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
await waitFor(() => {
|
|
51
|
+
expect(screen.getByTestId('app-provider')).toBeInTheDocument()
|
|
52
|
+
expect(screen.getByTestId('app-provider')).toHaveAttribute(
|
|
53
|
+
'data-site-id',
|
|
54
|
+
'TEST_SITE_ID',
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(screen.getByText('Test Content')).toBeInTheDocument()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should render App Router provider when router="app"', async () => {
|
|
62
|
+
render(
|
|
63
|
+
<NextFathomProvider siteId="TEST_SITE_ID" router="app">
|
|
64
|
+
<div>Test Content</div>
|
|
65
|
+
</NextFathomProvider>,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(screen.getByTestId('app-provider')).toBeInTheDocument()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(screen.queryByTestId('pages-provider')).not.toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should render Pages Router provider when router="pages"', async () => {
|
|
76
|
+
render(
|
|
77
|
+
<NextFathomProvider siteId="TEST_SITE_ID" router="pages">
|
|
78
|
+
<div>Test Content</div>
|
|
79
|
+
</NextFathomProvider>,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
await waitFor(() => {
|
|
83
|
+
expect(screen.getByTestId('pages-provider')).toBeInTheDocument()
|
|
84
|
+
expect(screen.getByTestId('pages-provider')).toHaveAttribute(
|
|
85
|
+
'data-site-id',
|
|
86
|
+
'TEST_SITE_ID',
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
expect(screen.queryByTestId('app-provider')).not.toBeInTheDocument()
|
|
91
|
+
expect(screen.getByText('Test Content')).toBeInTheDocument()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should render custom fallback while loading', async () => {
|
|
95
|
+
render(
|
|
96
|
+
<NextFathomProvider
|
|
97
|
+
siteId="TEST_SITE_ID"
|
|
98
|
+
fallback={<div>Loading...</div>}
|
|
99
|
+
>
|
|
100
|
+
<div>Test Content</div>
|
|
101
|
+
</NextFathomProvider>,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
// Suspense fallback may be shown briefly, but provider should load quickly
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect(screen.getByTestId('app-provider')).toBeInTheDocument()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should pass all props to the underlying provider', async () => {
|
|
111
|
+
const clientOptions = { honorDNT: true }
|
|
112
|
+
render(
|
|
113
|
+
<NextFathomProvider
|
|
114
|
+
siteId="TEST_SITE_ID"
|
|
115
|
+
router="app"
|
|
116
|
+
clientOptions={clientOptions}
|
|
117
|
+
disableAutoTrack
|
|
118
|
+
>
|
|
119
|
+
<div>Test Content</div>
|
|
120
|
+
</NextFathomProvider>,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(screen.getByTestId('app-provider')).toBeInTheDocument()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should have displayName', () => {
|
|
129
|
+
expect(NextFathomProvider.displayName).toBe('NextFathomProvider')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { Suspense, lazy, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { NextFathomProviderProps } from './types'
|
|
4
|
+
|
|
5
|
+
export interface NextFathomProviderComponentProps extends NextFathomProviderProps {
|
|
6
|
+
/**
|
|
7
|
+
* Router type to use
|
|
8
|
+
* @default 'app'
|
|
9
|
+
*/
|
|
10
|
+
router?: 'pages' | 'app'
|
|
11
|
+
/**
|
|
12
|
+
* Fallback component to show while loading the router provider
|
|
13
|
+
* @default null
|
|
14
|
+
*/
|
|
15
|
+
fallback?: React.ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Dynamically import providers to enable code-splitting
|
|
19
|
+
const NextFathomProviderAppLazy = lazy(
|
|
20
|
+
async () => await import('./NextFathomProviderApp'),
|
|
21
|
+
)
|
|
22
|
+
const NextFathomProviderPagesLazy = lazy(
|
|
23
|
+
async () => await import('./NextFathomProviderPages'),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Unified provider component that conditionally renders the appropriate
|
|
28
|
+
* Next.js router provider based on the `router` prop.
|
|
29
|
+
* Providers are dynamically loaded to avoid bundling both router types.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* // App Router (default)
|
|
34
|
+
* <NextFathomProvider siteId="YOUR_SITE_ID" router="app">
|
|
35
|
+
* <App>{children}</App>
|
|
36
|
+
* </NextFathomProvider>
|
|
37
|
+
*
|
|
38
|
+
* // Pages Router
|
|
39
|
+
* <NextFathomProvider siteId="YOUR_SITE_ID" router="pages">
|
|
40
|
+
* <App>{children}</App>
|
|
41
|
+
* </NextFathomProvider>
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const NextFathomProvider: React.FC<NextFathomProviderComponentProps> = ({
|
|
45
|
+
router = 'app',
|
|
46
|
+
fallback = null,
|
|
47
|
+
...props
|
|
48
|
+
}) => {
|
|
49
|
+
const Provider = useMemo(() => {
|
|
50
|
+
return router === 'pages'
|
|
51
|
+
? NextFathomProviderPagesLazy
|
|
52
|
+
: NextFathomProviderAppLazy
|
|
53
|
+
}, [router])
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Suspense fallback={fallback}>
|
|
57
|
+
<Provider {...props} />
|
|
58
|
+
</Suspense>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
NextFathomProvider.displayName = 'NextFathomProvider'
|