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,330 @@
|
|
|
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 NextFathomProviderPages from './NextFathomProviderPages'
|
|
8
|
+
import { useFathom } from '../hooks/useFathom'
|
|
9
|
+
|
|
10
|
+
// Mock Next.js Pages Router hook
|
|
11
|
+
const mockRouter = {
|
|
12
|
+
pathname: '/test-page',
|
|
13
|
+
query: { foo: 'bar' },
|
|
14
|
+
asPath: '/test-page?foo=bar',
|
|
15
|
+
isReady: true,
|
|
16
|
+
events: {
|
|
17
|
+
on: vi.fn(),
|
|
18
|
+
off: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
vi.mock('next/router', () => ({
|
|
23
|
+
useRouter: vi.fn(() => mockRouter),
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
// Mock fathom-client
|
|
27
|
+
vi.mock('fathom-client', async () => {
|
|
28
|
+
const mockFathomClient = {
|
|
29
|
+
trackEvent: vi.fn(),
|
|
30
|
+
trackPageview: vi.fn(),
|
|
31
|
+
trackGoal: vi.fn(),
|
|
32
|
+
load: vi.fn(),
|
|
33
|
+
setSite: vi.fn(),
|
|
34
|
+
blockTrackingForMe: vi.fn(),
|
|
35
|
+
enableTrackingForMe: vi.fn(),
|
|
36
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
default: mockFathomClient,
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('NextFathomProviderPages', () => {
|
|
45
|
+
const mockClient = {
|
|
46
|
+
trackEvent: vi.fn(),
|
|
47
|
+
trackPageview: vi.fn(),
|
|
48
|
+
trackGoal: vi.fn(),
|
|
49
|
+
load: vi.fn(),
|
|
50
|
+
setSite: vi.fn(),
|
|
51
|
+
blockTrackingForMe: vi.fn(),
|
|
52
|
+
enableTrackingForMe: vi.fn(),
|
|
53
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.clearAllMocks()
|
|
58
|
+
mockRouter.isReady = true
|
|
59
|
+
mockRouter.events.on = vi.fn()
|
|
60
|
+
mockRouter.events.off = vi.fn()
|
|
61
|
+
// Reset window.location
|
|
62
|
+
Object.defineProperty(window, 'location', {
|
|
63
|
+
value: {
|
|
64
|
+
origin: 'https://example.com',
|
|
65
|
+
href: 'https://example.com/test-page?foo=bar',
|
|
66
|
+
},
|
|
67
|
+
writable: true,
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should load Fathom on mount when siteId is provided', async () => {
|
|
72
|
+
const loadSpy = vi.fn()
|
|
73
|
+
const client = { ...mockClient, load: loadSpy }
|
|
74
|
+
|
|
75
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
76
|
+
<NextFathomProviderPages client={client} siteId="TEST_SITE_ID">
|
|
77
|
+
{children}
|
|
78
|
+
</NextFathomProviderPages>
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
renderHook(() => useFathom(), { wrapper })
|
|
82
|
+
|
|
83
|
+
await waitFor(() => {
|
|
84
|
+
expect(loadSpy).toHaveBeenCalledWith('TEST_SITE_ID', undefined)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should load Fathom with clientOptions', async () => {
|
|
89
|
+
const loadSpy = vi.fn()
|
|
90
|
+
const clientOptions = { honorDNT: true }
|
|
91
|
+
const client = { ...mockClient, load: loadSpy }
|
|
92
|
+
|
|
93
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
94
|
+
<NextFathomProviderPages
|
|
95
|
+
client={client}
|
|
96
|
+
siteId="TEST_SITE_ID"
|
|
97
|
+
clientOptions={clientOptions}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</NextFathomProviderPages>
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
renderHook(() => useFathom(), { wrapper })
|
|
104
|
+
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect(loadSpy).toHaveBeenCalledWith('TEST_SITE_ID', clientOptions)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should track initial pageview when router is ready', async () => {
|
|
111
|
+
const trackPageviewSpy = vi.fn()
|
|
112
|
+
const client = { ...mockClient, trackPageview: trackPageviewSpy }
|
|
113
|
+
|
|
114
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
115
|
+
<NextFathomProviderPages client={client} siteId="TEST_SITE_ID">
|
|
116
|
+
{children}
|
|
117
|
+
</NextFathomProviderPages>
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
renderHook(() => useFathom(), { wrapper })
|
|
121
|
+
|
|
122
|
+
await waitFor(() => {
|
|
123
|
+
expect(trackPageviewSpy).toHaveBeenCalled()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect(trackPageviewSpy).toHaveBeenCalledWith({
|
|
127
|
+
url: 'https://example.com/test-page?foo=bar',
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should not track initial pageview when router is not ready', async () => {
|
|
132
|
+
mockRouter.isReady = false
|
|
133
|
+
const trackPageviewSpy = vi.fn()
|
|
134
|
+
const client = { ...mockClient, trackPageview: trackPageviewSpy }
|
|
135
|
+
|
|
136
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
137
|
+
<NextFathomProviderPages client={client} siteId="TEST_SITE_ID">
|
|
138
|
+
{children}
|
|
139
|
+
</NextFathomProviderPages>
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
renderHook(() => useFathom(), { wrapper })
|
|
143
|
+
|
|
144
|
+
// Wait a bit to ensure no tracking happens
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
146
|
+
|
|
147
|
+
expect(trackPageviewSpy).not.toHaveBeenCalled()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should register route change listener', async () => {
|
|
151
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
152
|
+
<NextFathomProviderPages client={mockClient} siteId="TEST_SITE_ID">
|
|
153
|
+
{children}
|
|
154
|
+
</NextFathomProviderPages>
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
renderHook(() => useFathom(), { wrapper })
|
|
158
|
+
|
|
159
|
+
await waitFor(() => {
|
|
160
|
+
expect(mockRouter.events.on).toHaveBeenCalledWith(
|
|
161
|
+
'routeChangeComplete',
|
|
162
|
+
expect.any(Function),
|
|
163
|
+
)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should unregister route change listener on unmount', async () => {
|
|
168
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
169
|
+
<NextFathomProviderPages client={mockClient} siteId="TEST_SITE_ID">
|
|
170
|
+
{children}
|
|
171
|
+
</NextFathomProviderPages>
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const { unmount } = renderHook(() => useFathom(), { wrapper })
|
|
175
|
+
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(mockRouter.events.on).toHaveBeenCalled()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
unmount()
|
|
181
|
+
|
|
182
|
+
expect(mockRouter.events.off).toHaveBeenCalledWith(
|
|
183
|
+
'routeChangeComplete',
|
|
184
|
+
expect.any(Function),
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should track pageview on route change', async () => {
|
|
189
|
+
const trackPageviewSpy = vi.fn()
|
|
190
|
+
const client = { ...mockClient, trackPageview: trackPageviewSpy }
|
|
191
|
+
|
|
192
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
193
|
+
<NextFathomProviderPages client={client} siteId="TEST_SITE_ID">
|
|
194
|
+
{children}
|
|
195
|
+
</NextFathomProviderPages>
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
renderHook(() => useFathom(), { wrapper })
|
|
199
|
+
|
|
200
|
+
await waitFor(() => {
|
|
201
|
+
expect(mockRouter.events.on).toHaveBeenCalled()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Get the route change handler
|
|
205
|
+
const routeChangeHandler = mockRouter.events.on.mock.calls[0][1]
|
|
206
|
+
routeChangeHandler('/new-page')
|
|
207
|
+
|
|
208
|
+
expect(trackPageviewSpy).toHaveBeenCalledWith({
|
|
209
|
+
url: 'https://example.com/new-page',
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should not track pageview when disableAutoTrack is true', async () => {
|
|
214
|
+
const trackPageviewSpy = vi.fn()
|
|
215
|
+
const client = { ...mockClient, trackPageview: trackPageviewSpy }
|
|
216
|
+
|
|
217
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
218
|
+
<NextFathomProviderPages
|
|
219
|
+
client={client}
|
|
220
|
+
siteId="TEST_SITE_ID"
|
|
221
|
+
disableAutoTrack
|
|
222
|
+
>
|
|
223
|
+
{children}
|
|
224
|
+
</NextFathomProviderPages>
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
renderHook(() => useFathom(), { wrapper })
|
|
228
|
+
|
|
229
|
+
// Wait a bit to ensure no tracking happens
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
231
|
+
|
|
232
|
+
expect(trackPageviewSpy).not.toHaveBeenCalled()
|
|
233
|
+
expect(mockRouter.events.on).not.toHaveBeenCalled()
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should use provided client', () => {
|
|
237
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
238
|
+
<NextFathomProviderPages client={mockClient} siteId="TEST_SITE_ID">
|
|
239
|
+
{children}
|
|
240
|
+
</NextFathomProviderPages>
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
const { result } = renderHook(() => useFathom(), { wrapper })
|
|
244
|
+
|
|
245
|
+
expect(result.current.client).toBe(mockClient)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should merge defaultPageviewOptions', async () => {
|
|
249
|
+
const trackPageviewSpy = vi.fn()
|
|
250
|
+
const client = { ...mockClient, trackPageview: trackPageviewSpy }
|
|
251
|
+
|
|
252
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
253
|
+
<NextFathomProviderPages
|
|
254
|
+
client={client}
|
|
255
|
+
siteId="TEST_SITE_ID"
|
|
256
|
+
defaultPageviewOptions={{ referrer: 'https://example.com' }}
|
|
257
|
+
>
|
|
258
|
+
{children}
|
|
259
|
+
</NextFathomProviderPages>
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
renderHook(() => useFathom(), { wrapper })
|
|
263
|
+
|
|
264
|
+
await waitFor(() => {
|
|
265
|
+
expect(trackPageviewSpy).toHaveBeenCalled()
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
expect(trackPageviewSpy).toHaveBeenCalledWith({
|
|
269
|
+
referrer: 'https://example.com',
|
|
270
|
+
url: 'https://example.com/test-page?foo=bar',
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should support deprecated trackDefaultOptions', async () => {
|
|
275
|
+
const trackPageviewSpy = vi.fn()
|
|
276
|
+
const client = { ...mockClient, trackPageview: trackPageviewSpy }
|
|
277
|
+
|
|
278
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
279
|
+
<NextFathomProviderPages
|
|
280
|
+
client={client}
|
|
281
|
+
siteId="TEST_SITE_ID"
|
|
282
|
+
trackDefaultOptions={{ referrer: 'https://example.com' }}
|
|
283
|
+
>
|
|
284
|
+
{children}
|
|
285
|
+
</NextFathomProviderPages>
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
renderHook(() => useFathom(), { wrapper })
|
|
289
|
+
|
|
290
|
+
await waitFor(() => {
|
|
291
|
+
expect(trackPageviewSpy).toHaveBeenCalled()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
expect(trackPageviewSpy).toHaveBeenCalledWith({
|
|
295
|
+
referrer: 'https://example.com',
|
|
296
|
+
url: 'https://example.com/test-page?foo=bar',
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('should prioritize defaultPageviewOptions over trackDefaultOptions', async () => {
|
|
301
|
+
const trackPageviewSpy = vi.fn()
|
|
302
|
+
const client = { ...mockClient, trackPageview: trackPageviewSpy }
|
|
303
|
+
|
|
304
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
305
|
+
<NextFathomProviderPages
|
|
306
|
+
client={client}
|
|
307
|
+
siteId="TEST_SITE_ID"
|
|
308
|
+
trackDefaultOptions={{ referrer: 'old' }}
|
|
309
|
+
defaultPageviewOptions={{ referrer: 'new' }}
|
|
310
|
+
>
|
|
311
|
+
{children}
|
|
312
|
+
</NextFathomProviderPages>
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
renderHook(() => useFathom(), { wrapper })
|
|
316
|
+
|
|
317
|
+
await waitFor(() => {
|
|
318
|
+
expect(trackPageviewSpy).toHaveBeenCalled()
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
expect(trackPageviewSpy).toHaveBeenCalledWith({
|
|
322
|
+
referrer: 'new',
|
|
323
|
+
url: 'https://example.com/test-page?foo=bar',
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('should have displayName', () => {
|
|
328
|
+
expect(NextFathomProviderPages.displayName).toBe('NextFathomProviderPages')
|
|
329
|
+
})
|
|
330
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
import * as Fathom from 'fathom-client'
|
|
4
|
+
import type { PageViewOptions } from 'fathom-client'
|
|
5
|
+
import { useRouter } from 'next/router'
|
|
6
|
+
|
|
7
|
+
import { FathomProvider } from '../FathomProvider'
|
|
8
|
+
import type { NextFathomProviderProps } from './types'
|
|
9
|
+
import { useFathom } from '../hooks/useFathom'
|
|
10
|
+
|
|
11
|
+
const NextFathomProviderPages: React.FC<NextFathomProviderProps> = ({
|
|
12
|
+
children,
|
|
13
|
+
client: providedClient,
|
|
14
|
+
clientOptions,
|
|
15
|
+
disableAutoTrack = false,
|
|
16
|
+
siteId,
|
|
17
|
+
trackDefaultOptions,
|
|
18
|
+
defaultPageviewOptions: providedDefaultPageviewOptions,
|
|
19
|
+
}) => {
|
|
20
|
+
const router = useRouter()
|
|
21
|
+
const hasTrackedInitialPageview = useRef(false)
|
|
22
|
+
const parentContext = useFathom()
|
|
23
|
+
|
|
24
|
+
// Use provided client or fall back to parent client or default Fathom
|
|
25
|
+
const client = useMemo(
|
|
26
|
+
() => providedClient ?? parentContext.client ?? Fathom,
|
|
27
|
+
[providedClient, parentContext.client],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// Support both deprecated trackDefaultOptions and new defaultPageviewOptions
|
|
31
|
+
// Priority: providedDefaultPageviewOptions > trackDefaultOptions > parent defaultPageviewOptions
|
|
32
|
+
const defaultPageviewOptions = useMemo(
|
|
33
|
+
() =>
|
|
34
|
+
providedDefaultPageviewOptions ??
|
|
35
|
+
trackDefaultOptions ??
|
|
36
|
+
parentContext.defaultPageviewOptions,
|
|
37
|
+
[
|
|
38
|
+
providedDefaultPageviewOptions,
|
|
39
|
+
trackDefaultOptions,
|
|
40
|
+
parentContext.defaultPageviewOptions,
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const trackPageview = useCallback(
|
|
45
|
+
(options?: PageViewOptions) => {
|
|
46
|
+
if (siteId !== undefined && client !== undefined) {
|
|
47
|
+
client.trackPageview({
|
|
48
|
+
...defaultPageviewOptions,
|
|
49
|
+
...options,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
[client, siteId, defaultPageviewOptions],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Initialize Fathom on mount
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (siteId !== undefined && client !== undefined) {
|
|
59
|
+
client.load(siteId, clientOptions)
|
|
60
|
+
}
|
|
61
|
+
}, [client, clientOptions, siteId])
|
|
62
|
+
|
|
63
|
+
// Track pageviews on route changes
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (siteId === undefined || disableAutoTrack) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handleRouteChangeComplete = (url: string): void => {
|
|
70
|
+
trackPageview({
|
|
71
|
+
url: window.location.origin + url,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
router.events.on('routeChangeComplete', handleRouteChangeComplete)
|
|
76
|
+
|
|
77
|
+
return () => {
|
|
78
|
+
router.events.off('routeChangeComplete', handleRouteChangeComplete)
|
|
79
|
+
}
|
|
80
|
+
}, [router.events, siteId, disableAutoTrack, trackPageview])
|
|
81
|
+
|
|
82
|
+
// Track initial pageview (routeChangeComplete doesn't fire on initial load)
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (
|
|
85
|
+
siteId !== undefined &&
|
|
86
|
+
!disableAutoTrack &&
|
|
87
|
+
router.isReady &&
|
|
88
|
+
!hasTrackedInitialPageview.current
|
|
89
|
+
) {
|
|
90
|
+
hasTrackedInitialPageview.current = true
|
|
91
|
+
trackPageview({
|
|
92
|
+
url: window.location.href,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}, [siteId, disableAutoTrack, router.isReady, trackPageview])
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<FathomProvider
|
|
99
|
+
client={client}
|
|
100
|
+
clientOptions={clientOptions}
|
|
101
|
+
siteId={siteId}
|
|
102
|
+
defaultPageviewOptions={defaultPageviewOptions}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
</FathomProvider>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
NextFathomProviderPages.displayName = 'NextFathomProviderPages'
|
|
110
|
+
|
|
111
|
+
export default NextFathomProviderPages
|
|
112
|
+
export { NextFathomProviderPages }
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { render, screen } from '@testing-library/react'
|
|
6
|
+
|
|
7
|
+
import { withAppRouter } from './withAppRouter'
|
|
8
|
+
|
|
9
|
+
// Mock NextFathomProviderApp
|
|
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
|
+
describe('withAppRouter', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should wrap component with NextFathomProviderApp', () => {
|
|
30
|
+
const TestComponent = ({ name }: { name: string }) => (
|
|
31
|
+
<div>Hello {name}</div>
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const WrappedComponent = withAppRouter(TestComponent, {
|
|
35
|
+
siteId: 'TEST_SITE_ID',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
render(<WrappedComponent name="World" />)
|
|
39
|
+
|
|
40
|
+
expect(screen.getByTestId('app-provider')).toBeInTheDocument()
|
|
41
|
+
expect(screen.getByTestId('app-provider')).toHaveAttribute(
|
|
42
|
+
'data-site-id',
|
|
43
|
+
'TEST_SITE_ID',
|
|
44
|
+
)
|
|
45
|
+
expect(screen.getByText('Hello World')).toBeInTheDocument()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should pass props to wrapped component', () => {
|
|
49
|
+
const TestComponent = ({ count }: { count: number }) => (
|
|
50
|
+
<div>Count: {count}</div>
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const WrappedComponent = withAppRouter(TestComponent)
|
|
54
|
+
|
|
55
|
+
render(<WrappedComponent count={42} />)
|
|
56
|
+
|
|
57
|
+
expect(screen.getByText('Count: 42')).toBeInTheDocument()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should pass provider props to NextFathomProviderApp', () => {
|
|
61
|
+
const TestComponent = () => <div>Test</div>
|
|
62
|
+
|
|
63
|
+
const WrappedComponent = withAppRouter(TestComponent, {
|
|
64
|
+
siteId: 'TEST_SITE_ID',
|
|
65
|
+
disableAutoTrack: true,
|
|
66
|
+
clientOptions: { honorDNT: true },
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
render(<WrappedComponent />)
|
|
70
|
+
|
|
71
|
+
expect(screen.getByTestId('app-provider')).toBeInTheDocument()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should work without provider props', () => {
|
|
75
|
+
const TestComponent = () => <div>Test</div>
|
|
76
|
+
|
|
77
|
+
const WrappedComponent = withAppRouter(TestComponent)
|
|
78
|
+
|
|
79
|
+
render(<WrappedComponent />)
|
|
80
|
+
|
|
81
|
+
expect(screen.getByTestId('app-provider')).toBeInTheDocument()
|
|
82
|
+
expect(screen.getByText('Test')).toBeInTheDocument()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should set displayName correctly', () => {
|
|
86
|
+
const TestComponent = () => <div>Test</div>
|
|
87
|
+
TestComponent.displayName = 'TestComponent'
|
|
88
|
+
|
|
89
|
+
const WrappedComponent = withAppRouter(TestComponent)
|
|
90
|
+
|
|
91
|
+
expect(WrappedComponent.displayName).toBe('withAppRouter(TestComponent)')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should set displayName with component name when displayName is not set', () => {
|
|
95
|
+
function TestComponent() {
|
|
96
|
+
return <div>Test</div>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const WrappedComponent = withAppRouter(TestComponent)
|
|
100
|
+
|
|
101
|
+
expect(WrappedComponent.displayName).toBe('withAppRouter(TestComponent)')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should set displayName with Component fallback', () => {
|
|
105
|
+
const TestComponent = () => <div>Test</div>
|
|
106
|
+
// Remove displayName and name
|
|
107
|
+
Object.defineProperty(TestComponent, 'name', { value: '' })
|
|
108
|
+
|
|
109
|
+
const WrappedComponent = withAppRouter(TestComponent)
|
|
110
|
+
|
|
111
|
+
expect(WrappedComponent.displayName).toBe('withAppRouter(Component)')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { ComponentType } from 'react'
|
|
3
|
+
|
|
4
|
+
import NextFathomProviderApp from '../NextFathomProviderApp'
|
|
5
|
+
import type { NextFathomProviderProps } from '../types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Higher-order component that wraps your Next.js App Router app with FathomProvider
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* // app/layout.tsx
|
|
13
|
+
* import { withAppRouter } from 'react-fathom/next'
|
|
14
|
+
*
|
|
15
|
+
* function RootLayout({ children }) {
|
|
16
|
+
* return (
|
|
17
|
+
* <html>
|
|
18
|
+
* <body>{children}</body>
|
|
19
|
+
* </html>
|
|
20
|
+
* )
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* export default withAppRouter(RootLayout, {
|
|
24
|
+
* siteId: 'YOUR_SITE_ID',
|
|
25
|
+
* clientOptions: {
|
|
26
|
+
* spa: 'auto',
|
|
27
|
+
* },
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
const withAppRouter = <P extends object>(
|
|
32
|
+
Component: ComponentType<P>,
|
|
33
|
+
providerProps?: NextFathomProviderProps,
|
|
34
|
+
): ComponentType<P> => {
|
|
35
|
+
const WithAppRouter: React.FC<P> = (props) => {
|
|
36
|
+
return (
|
|
37
|
+
<NextFathomProviderApp {...providerProps}>
|
|
38
|
+
<Component {...props} />
|
|
39
|
+
</NextFathomProviderApp>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
WithAppRouter.displayName = `withAppRouter(${Component.displayName || Component.name || 'Component'})`
|
|
44
|
+
|
|
45
|
+
return WithAppRouter
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { withAppRouter }
|