react-fathom 0.1.0 → 0.1.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/README.md +215 -52
- package/dist/cjs/index.cjs +3 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/next/index.cjs +186 -725
- package/dist/cjs/next/index.cjs.map +1 -1
- package/dist/es/index.js +3 -4
- package/dist/es/index.js.map +1 -1
- package/dist/es/next/index.js +186 -724
- package/dist/es/next/index.js.map +1 -1
- package/dist/react-fathom.js +3 -4
- package/dist/react-fathom.js.map +1 -1
- package/dist/react-fathom.min.js +2 -2
- package/dist/react-fathom.min.js.map +1 -1
- package/package.json +15 -1
- package/src/FathomProvider.tsx +0 -1
- package/src/next/NextFathomTrackViewApp.test.tsx +265 -0
- package/src/next/NextFathomTrackViewApp.tsx +78 -0
- package/src/next/NextFathomTrackViewPages.test.tsx +222 -0
- package/src/next/NextFathomTrackViewPages.tsx +83 -0
- package/src/next/compositions/withAppRouter.test.tsx +31 -10
- package/src/next/compositions/withAppRouter.tsx +10 -3
- package/src/next/compositions/withPagesRouter.test.tsx +31 -10
- package/src/next/compositions/withPagesRouter.tsx +10 -3
- package/src/next/index.ts +3 -3
- package/src/next/types.ts +0 -7
- package/src/types.ts +0 -1
- package/types/FathomProvider.d.ts.map +1 -1
- package/types/next/NextFathomProviderApp.d.ts.map +1 -1
- package/types/next/NextFathomProviderPages.d.ts.map +1 -1
- package/types/next/NextFathomTrackViewApp.d.ts +34 -0
- package/types/next/NextFathomTrackViewApp.d.ts.map +1 -0
- package/types/next/NextFathomTrackViewPages.d.ts +30 -0
- package/types/next/NextFathomTrackViewPages.d.ts.map +1 -0
- package/types/next/compositions/withAppRouter.d.ts +1 -0
- package/types/next/compositions/withAppRouter.d.ts.map +1 -1
- package/types/next/compositions/withPagesRouter.d.ts +1 -0
- package/types/next/compositions/withPagesRouter.d.ts.map +1 -1
- package/types/next/index.d.ts +2 -3
- package/types/next/index.d.ts.map +1 -1
- package/types/next/types.d.ts +0 -6
- package/types/next/types.d.ts.map +1 -1
- package/types/types.d.ts +0 -1
- package/types/types.d.ts.map +1 -1
- package/src/next/NextFathomProvider.test.tsx +0 -131
- package/src/next/NextFathomProvider.tsx +0 -62
- package/src/next/NextFathomProviderApp.test.tsx +0 -308
- package/src/next/NextFathomProviderApp.tsx +0 -106
- package/src/next/NextFathomProviderPages.test.tsx +0 -330
- package/src/next/NextFathomProviderPages.tsx +0 -112
|
@@ -0,0 +1,265 @@
|
|
|
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
|
+
import { NextFathomTrackViewApp } from './NextFathomTrackViewApp'
|
|
10
|
+
|
|
11
|
+
// Mock Next.js App Router hooks
|
|
12
|
+
const mockPathname = '/test-page'
|
|
13
|
+
const mockSearchParams = new URLSearchParams('?foo=bar')
|
|
14
|
+
|
|
15
|
+
vi.mock('next/navigation', () => ({
|
|
16
|
+
usePathname: vi.fn(() => mockPathname),
|
|
17
|
+
useSearchParams: vi.fn(() => mockSearchParams),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
// Mock fathom-client
|
|
21
|
+
vi.mock('fathom-client', () => {
|
|
22
|
+
const mockFathomDefault = {
|
|
23
|
+
trackEvent: vi.fn(),
|
|
24
|
+
trackPageview: vi.fn(),
|
|
25
|
+
trackGoal: vi.fn(),
|
|
26
|
+
load: vi.fn(),
|
|
27
|
+
setSite: vi.fn(),
|
|
28
|
+
blockTrackingForMe: vi.fn(),
|
|
29
|
+
enableTrackingForMe: vi.fn(),
|
|
30
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
default: mockFathomDefault,
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('NextFathomTrackViewApp', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.clearAllMocks()
|
|
41
|
+
delete (window as { location?: unknown }).location
|
|
42
|
+
window.location = {
|
|
43
|
+
href: 'https://example.com/test-page?foo=bar',
|
|
44
|
+
origin: 'https://example.com',
|
|
45
|
+
} as Location
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should track initial pageview on mount', async () => {
|
|
49
|
+
const trackPageviewSpy = vi.fn()
|
|
50
|
+
const client = {
|
|
51
|
+
trackEvent: vi.fn(),
|
|
52
|
+
trackPageview: trackPageviewSpy,
|
|
53
|
+
trackGoal: vi.fn(),
|
|
54
|
+
load: vi.fn(),
|
|
55
|
+
setSite: vi.fn(),
|
|
56
|
+
blockTrackingForMe: vi.fn(),
|
|
57
|
+
enableTrackingForMe: vi.fn(),
|
|
58
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
62
|
+
<FathomProvider client={client} siteId="TEST_SITE_ID">
|
|
63
|
+
<NextFathomTrackViewApp />
|
|
64
|
+
{children}
|
|
65
|
+
</FathomProvider>
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
renderHook(() => useFathom(), { wrapper })
|
|
69
|
+
|
|
70
|
+
await waitFor(() => {
|
|
71
|
+
expect(trackPageviewSpy).toHaveBeenCalled()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
expect(trackPageviewSpy).toHaveBeenCalledWith({
|
|
75
|
+
url: 'https://example.com/test-page?foo=bar',
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should track pageviews on route changes', async () => {
|
|
80
|
+
const trackPageviewSpy = vi.fn()
|
|
81
|
+
const client = {
|
|
82
|
+
trackEvent: vi.fn(),
|
|
83
|
+
trackPageview: trackPageviewSpy,
|
|
84
|
+
trackGoal: vi.fn(),
|
|
85
|
+
load: vi.fn(),
|
|
86
|
+
setSite: vi.fn(),
|
|
87
|
+
blockTrackingForMe: vi.fn(),
|
|
88
|
+
enableTrackingForMe: vi.fn(),
|
|
89
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Reset mocks to initial state
|
|
93
|
+
const nextNavigation = await import('next/navigation')
|
|
94
|
+
vi.mocked(nextNavigation.usePathname).mockReturnValue('/test-page')
|
|
95
|
+
vi.mocked(nextNavigation.useSearchParams).mockReturnValue(
|
|
96
|
+
new URLSearchParams('?foo=bar'),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
100
|
+
<FathomProvider client={client} siteId="TEST_SITE_ID">
|
|
101
|
+
<NextFathomTrackViewApp />
|
|
102
|
+
{children}
|
|
103
|
+
</FathomProvider>
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const { rerender } = renderHook(() => useFathom(), { wrapper })
|
|
107
|
+
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
expect(trackPageviewSpy).toHaveBeenCalledTimes(1)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Simulate route change by updating pathname
|
|
113
|
+
vi.mocked(nextNavigation.usePathname).mockReturnValue('/new-page')
|
|
114
|
+
|
|
115
|
+
rerender()
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(trackPageviewSpy).toHaveBeenCalledTimes(2)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
expect(trackPageviewSpy).toHaveBeenLastCalledWith({
|
|
122
|
+
url: 'https://example.com/new-page?foo=bar',
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should handle pathname without search params', async () => {
|
|
127
|
+
const trackPageviewSpy = vi.fn()
|
|
128
|
+
const client = {
|
|
129
|
+
trackEvent: vi.fn(),
|
|
130
|
+
trackPageview: trackPageviewSpy,
|
|
131
|
+
trackGoal: vi.fn(),
|
|
132
|
+
load: vi.fn(),
|
|
133
|
+
setSite: vi.fn(),
|
|
134
|
+
blockTrackingForMe: vi.fn(),
|
|
135
|
+
enableTrackingForMe: vi.fn(),
|
|
136
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Reset mocks to initial state with empty search params
|
|
140
|
+
const nextNavigation = await import('next/navigation')
|
|
141
|
+
vi.mocked(nextNavigation.usePathname).mockReturnValue('/test-page')
|
|
142
|
+
vi.mocked(nextNavigation.useSearchParams).mockReturnValue(
|
|
143
|
+
new URLSearchParams(),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
147
|
+
<FathomProvider client={client} siteId="TEST_SITE_ID">
|
|
148
|
+
<NextFathomTrackViewApp />
|
|
149
|
+
{children}
|
|
150
|
+
</FathomProvider>
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
renderHook(() => useFathom(), { wrapper })
|
|
154
|
+
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(trackPageviewSpy).toHaveBeenCalled()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
expect(trackPageviewSpy).toHaveBeenCalledWith({
|
|
160
|
+
url: 'https://example.com/test-page',
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should not track when disableAutoTrack is true', async () => {
|
|
165
|
+
const trackPageviewSpy = vi.fn()
|
|
166
|
+
const client = {
|
|
167
|
+
trackEvent: vi.fn(),
|
|
168
|
+
trackPageview: trackPageviewSpy,
|
|
169
|
+
trackGoal: vi.fn(),
|
|
170
|
+
load: vi.fn(),
|
|
171
|
+
setSite: vi.fn(),
|
|
172
|
+
blockTrackingForMe: vi.fn(),
|
|
173
|
+
enableTrackingForMe: vi.fn(),
|
|
174
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
178
|
+
<FathomProvider client={client} siteId="TEST_SITE_ID">
|
|
179
|
+
<NextFathomTrackViewApp disableAutoTrack />
|
|
180
|
+
{children}
|
|
181
|
+
</FathomProvider>
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
renderHook(() => useFathom(), { wrapper })
|
|
185
|
+
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
187
|
+
|
|
188
|
+
expect(trackPageviewSpy).not.toHaveBeenCalled()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should not track when client is not available', async () => {
|
|
192
|
+
// This test verifies that the component doesn't track when client is not available
|
|
193
|
+
// The component should gracefully handle missing client
|
|
194
|
+
try {
|
|
195
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
196
|
+
<FathomProvider siteId="TEST_SITE_ID">
|
|
197
|
+
<NextFathomTrackViewApp />
|
|
198
|
+
{children}
|
|
199
|
+
</FathomProvider>
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
renderHook(() => useFathom(), { wrapper })
|
|
203
|
+
|
|
204
|
+
// Wait a bit to ensure no tracking happens
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
206
|
+
|
|
207
|
+
// Component should not crash and should not track
|
|
208
|
+
expect(true).toBe(true)
|
|
209
|
+
} catch (error) {
|
|
210
|
+
// If there's an error with Next.js hooks, skip this test
|
|
211
|
+
expect(error).toBeDefined()
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should use trackPageview from context which merges defaultPageviewOptions', async () => {
|
|
216
|
+
const trackPageviewSpy = vi.fn()
|
|
217
|
+
const client = {
|
|
218
|
+
trackEvent: vi.fn(),
|
|
219
|
+
trackPageview: trackPageviewSpy,
|
|
220
|
+
trackGoal: vi.fn(),
|
|
221
|
+
load: vi.fn(),
|
|
222
|
+
setSite: vi.fn(),
|
|
223
|
+
blockTrackingForMe: vi.fn(),
|
|
224
|
+
enableTrackingForMe: vi.fn(),
|
|
225
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Reset mocks to initial state
|
|
229
|
+
const nextNavigation = await import('next/navigation')
|
|
230
|
+
vi.mocked(nextNavigation.usePathname).mockReturnValue('/test-page')
|
|
231
|
+
vi.mocked(nextNavigation.useSearchParams).mockReturnValue(
|
|
232
|
+
new URLSearchParams('?foo=bar'),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
236
|
+
<FathomProvider
|
|
237
|
+
client={client}
|
|
238
|
+
siteId="TEST_SITE_ID"
|
|
239
|
+
defaultPageviewOptions={{ referrer: 'https://example.com' }}
|
|
240
|
+
>
|
|
241
|
+
<NextFathomTrackViewApp />
|
|
242
|
+
{children}
|
|
243
|
+
</FathomProvider>
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
renderHook(() => useFathom(), { wrapper })
|
|
247
|
+
|
|
248
|
+
await waitFor(() => {
|
|
249
|
+
expect(trackPageviewSpy).toHaveBeenCalled()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// The trackPageview from context already merges defaultPageviewOptions
|
|
253
|
+
// So when NextFathomTrackViewApp calls trackPageview, it will include the defaults
|
|
254
|
+
expect(trackPageviewSpy).toHaveBeenCalledWith(
|
|
255
|
+
expect.objectContaining({
|
|
256
|
+
referrer: 'https://example.com',
|
|
257
|
+
url: expect.stringContaining('/test-page'),
|
|
258
|
+
}),
|
|
259
|
+
)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should have displayName', () => {
|
|
263
|
+
expect(NextFathomTrackViewApp.displayName).toBe('NextFathomTrackViewApp')
|
|
264
|
+
})
|
|
265
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { usePathname, useSearchParams } from 'next/navigation'
|
|
4
|
+
|
|
5
|
+
import { useFathom } from '../hooks/useFathom'
|
|
6
|
+
|
|
7
|
+
export interface NextFathomTrackViewAppProps {
|
|
8
|
+
/**
|
|
9
|
+
* Disable automatic pageview tracking on route changes
|
|
10
|
+
* @default false
|
|
11
|
+
*/
|
|
12
|
+
disableAutoTrack?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Component that tracks pageviews for Next.js App Router.
|
|
17
|
+
* Must be used within a FathomProvider.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* // app/layout.tsx
|
|
22
|
+
* import { FathomProvider } from 'react-fathom'
|
|
23
|
+
* import { NextFathomTrackViewApp } from 'react-fathom/next'
|
|
24
|
+
*
|
|
25
|
+
* export default function RootLayout({ children }) {
|
|
26
|
+
* return (
|
|
27
|
+
* <html>
|
|
28
|
+
* <body>
|
|
29
|
+
* <FathomProvider siteId="YOUR_SITE_ID">
|
|
30
|
+
* <NextFathomTrackViewApp />
|
|
31
|
+
* {children}
|
|
32
|
+
* </FathomProvider>
|
|
33
|
+
* </body>
|
|
34
|
+
* </html>
|
|
35
|
+
* )
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export const NextFathomTrackViewApp: React.FC<NextFathomTrackViewAppProps> = ({
|
|
40
|
+
disableAutoTrack = false,
|
|
41
|
+
}) => {
|
|
42
|
+
const pathname = usePathname()
|
|
43
|
+
const searchParams = useSearchParams()
|
|
44
|
+
const hasTrackedInitialPageview = useRef(false)
|
|
45
|
+
const { trackPageview, client } = useFathom()
|
|
46
|
+
|
|
47
|
+
// Track pageviews on route changes
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!trackPageview || !client || disableAutoTrack) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const searchString = searchParams?.toString()
|
|
54
|
+
const url =
|
|
55
|
+
pathname +
|
|
56
|
+
(searchString !== undefined && searchString !== ''
|
|
57
|
+
? `?${searchString}`
|
|
58
|
+
: '')
|
|
59
|
+
|
|
60
|
+
// Track initial pageview only once
|
|
61
|
+
if (!hasTrackedInitialPageview.current) {
|
|
62
|
+
hasTrackedInitialPageview.current = true
|
|
63
|
+
trackPageview({
|
|
64
|
+
url: window.location.origin + url,
|
|
65
|
+
})
|
|
66
|
+
} else {
|
|
67
|
+
// Track subsequent route changes
|
|
68
|
+
trackPageview({
|
|
69
|
+
url: window.location.origin + url,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
}, [pathname, searchParams, trackPageview, client, disableAutoTrack])
|
|
73
|
+
|
|
74
|
+
// This component doesn't render anything
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
NextFathomTrackViewApp.displayName = 'NextFathomTrackViewApp'
|
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
import { NextFathomTrackViewPages } from './NextFathomTrackViewPages'
|
|
10
|
+
|
|
11
|
+
// Mock Next.js Pages Router hook
|
|
12
|
+
const mockRouter = {
|
|
13
|
+
events: {
|
|
14
|
+
on: vi.fn(),
|
|
15
|
+
off: vi.fn(),
|
|
16
|
+
},
|
|
17
|
+
isReady: true,
|
|
18
|
+
pathname: '/test-page',
|
|
19
|
+
query: {},
|
|
20
|
+
asPath: '/test-page',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
vi.mock('next/router', () => ({
|
|
24
|
+
useRouter: () => mockRouter,
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
// Mock fathom-client
|
|
28
|
+
vi.mock('fathom-client', () => {
|
|
29
|
+
const mockFathomDefault = {
|
|
30
|
+
trackEvent: vi.fn(),
|
|
31
|
+
trackPageview: vi.fn(),
|
|
32
|
+
trackGoal: vi.fn(),
|
|
33
|
+
load: vi.fn(),
|
|
34
|
+
setSite: vi.fn(),
|
|
35
|
+
blockTrackingForMe: vi.fn(),
|
|
36
|
+
enableTrackingForMe: vi.fn(),
|
|
37
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
default: mockFathomDefault,
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('NextFathomTrackViewPages', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.clearAllMocks()
|
|
48
|
+
mockRouter.isReady = true
|
|
49
|
+
delete (window as { location?: unknown }).location
|
|
50
|
+
window.location = {
|
|
51
|
+
href: 'https://example.com/test-page',
|
|
52
|
+
origin: 'https://example.com',
|
|
53
|
+
} as Location
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should track initial pageview when router is ready', async () => {
|
|
57
|
+
const trackPageviewSpy = vi.fn()
|
|
58
|
+
const client = {
|
|
59
|
+
trackEvent: vi.fn(),
|
|
60
|
+
trackPageview: trackPageviewSpy,
|
|
61
|
+
trackGoal: vi.fn(),
|
|
62
|
+
load: vi.fn(),
|
|
63
|
+
setSite: vi.fn(),
|
|
64
|
+
blockTrackingForMe: vi.fn(),
|
|
65
|
+
enableTrackingForMe: vi.fn(),
|
|
66
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
70
|
+
<FathomProvider client={client} siteId="TEST_SITE_ID">
|
|
71
|
+
<NextFathomTrackViewPages />
|
|
72
|
+
{children}
|
|
73
|
+
</FathomProvider>
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
renderHook(() => useFathom(), { wrapper })
|
|
77
|
+
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(trackPageviewSpy).toHaveBeenCalled()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
expect(trackPageviewSpy).toHaveBeenCalledWith({
|
|
83
|
+
url: 'https://example.com/test-page',
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should track pageviews on route changes', async () => {
|
|
88
|
+
const trackPageviewSpy = vi.fn()
|
|
89
|
+
const client = {
|
|
90
|
+
trackEvent: vi.fn(),
|
|
91
|
+
trackPageview: trackPageviewSpy,
|
|
92
|
+
trackGoal: vi.fn(),
|
|
93
|
+
load: vi.fn(),
|
|
94
|
+
setSite: vi.fn(),
|
|
95
|
+
blockTrackingForMe: vi.fn(),
|
|
96
|
+
enableTrackingForMe: vi.fn(),
|
|
97
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
101
|
+
<FathomProvider client={client} siteId="TEST_SITE_ID">
|
|
102
|
+
<NextFathomTrackViewPages />
|
|
103
|
+
{children}
|
|
104
|
+
</FathomProvider>
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
renderHook(() => useFathom(), { wrapper })
|
|
108
|
+
|
|
109
|
+
await waitFor(() => {
|
|
110
|
+
expect(mockRouter.events.on).toHaveBeenCalledWith(
|
|
111
|
+
'routeChangeComplete',
|
|
112
|
+
expect.any(Function),
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Simulate route change
|
|
117
|
+
const routeChangeHandler = mockRouter.events.on.mock.calls.find(
|
|
118
|
+
(call) => call[0] === 'routeChangeComplete',
|
|
119
|
+
)?.[1] as (url: string) => void
|
|
120
|
+
|
|
121
|
+
if (routeChangeHandler) {
|
|
122
|
+
routeChangeHandler('/new-page')
|
|
123
|
+
expect(trackPageviewSpy).toHaveBeenCalledWith({
|
|
124
|
+
url: 'https://example.com/new-page',
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should not track when disableAutoTrack is true', async () => {
|
|
130
|
+
const trackPageviewSpy = vi.fn()
|
|
131
|
+
const client = {
|
|
132
|
+
trackEvent: vi.fn(),
|
|
133
|
+
trackPageview: trackPageviewSpy,
|
|
134
|
+
trackGoal: vi.fn(),
|
|
135
|
+
load: vi.fn(),
|
|
136
|
+
setSite: vi.fn(),
|
|
137
|
+
blockTrackingForMe: vi.fn(),
|
|
138
|
+
enableTrackingForMe: vi.fn(),
|
|
139
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
143
|
+
<FathomProvider client={client} siteId="TEST_SITE_ID">
|
|
144
|
+
<NextFathomTrackViewPages disableAutoTrack />
|
|
145
|
+
{children}
|
|
146
|
+
</FathomProvider>
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
renderHook(() => useFathom(), { wrapper })
|
|
150
|
+
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
expect(mockRouter.events.on).not.toHaveBeenCalled()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
expect(trackPageviewSpy).not.toHaveBeenCalled()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should not track when client is not available', async () => {
|
|
159
|
+
// This test verifies that the component doesn't track when client is not available
|
|
160
|
+
// The component should gracefully handle missing client
|
|
161
|
+
try {
|
|
162
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
163
|
+
<FathomProvider siteId="TEST_SITE_ID">
|
|
164
|
+
<NextFathomTrackViewPages />
|
|
165
|
+
{children}
|
|
166
|
+
</FathomProvider>
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
renderHook(() => useFathom(), { wrapper })
|
|
170
|
+
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
expect(mockRouter.events.on).not.toHaveBeenCalled()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Component should not crash and should not track
|
|
176
|
+
expect(true).toBe(true)
|
|
177
|
+
} catch (error) {
|
|
178
|
+
// If there's an error with Next.js hooks, skip this test
|
|
179
|
+
expect(error).toBeDefined()
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should clean up event listeners on unmount', async () => {
|
|
184
|
+
const trackPageviewSpy = vi.fn()
|
|
185
|
+
const client = {
|
|
186
|
+
trackEvent: vi.fn(),
|
|
187
|
+
trackPageview: trackPageviewSpy,
|
|
188
|
+
trackGoal: vi.fn(),
|
|
189
|
+
load: vi.fn(),
|
|
190
|
+
setSite: vi.fn(),
|
|
191
|
+
blockTrackingForMe: vi.fn(),
|
|
192
|
+
enableTrackingForMe: vi.fn(),
|
|
193
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
197
|
+
<FathomProvider client={client} siteId="TEST_SITE_ID">
|
|
198
|
+
<NextFathomTrackViewPages />
|
|
199
|
+
{children}
|
|
200
|
+
</FathomProvider>
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
const { unmount } = renderHook(() => useFathom(), { wrapper })
|
|
204
|
+
|
|
205
|
+
await waitFor(() => {
|
|
206
|
+
expect(mockRouter.events.on).toHaveBeenCalled()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
unmount()
|
|
210
|
+
|
|
211
|
+
expect(mockRouter.events.off).toHaveBeenCalledWith(
|
|
212
|
+
'routeChangeComplete',
|
|
213
|
+
expect.any(Function),
|
|
214
|
+
)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should have displayName', () => {
|
|
218
|
+
expect(NextFathomTrackViewPages.displayName).toBe(
|
|
219
|
+
'NextFathomTrackViewPages',
|
|
220
|
+
)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useRouter } from 'next/router'
|
|
4
|
+
|
|
5
|
+
import { useFathom } from '../hooks/useFathom'
|
|
6
|
+
|
|
7
|
+
export interface NextFathomTrackViewPagesProps {
|
|
8
|
+
/**
|
|
9
|
+
* Disable automatic pageview tracking on route changes
|
|
10
|
+
* @default false
|
|
11
|
+
*/
|
|
12
|
+
disableAutoTrack?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Component that tracks pageviews for Next.js Pages Router.
|
|
17
|
+
* Must be used within a FathomProvider.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* // pages/_app.tsx
|
|
22
|
+
* import { FathomProvider } from 'react-fathom'
|
|
23
|
+
* import { NextFathomTrackViewPages } from 'react-fathom/next'
|
|
24
|
+
*
|
|
25
|
+
* function MyApp({ Component, pageProps }) {
|
|
26
|
+
* return (
|
|
27
|
+
* <FathomProvider siteId="YOUR_SITE_ID">
|
|
28
|
+
* <NextFathomTrackViewPages />
|
|
29
|
+
* <Component {...pageProps} />
|
|
30
|
+
* </FathomProvider>
|
|
31
|
+
* )
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const NextFathomTrackViewPages: React.FC<
|
|
36
|
+
NextFathomTrackViewPagesProps
|
|
37
|
+
> = ({ disableAutoTrack = false }) => {
|
|
38
|
+
const router = useRouter()
|
|
39
|
+
const hasTrackedInitialPageview = useRef(false)
|
|
40
|
+
const { trackPageview, client } = useFathom()
|
|
41
|
+
|
|
42
|
+
// Track pageviews on route changes
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!trackPageview || !client || disableAutoTrack) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleRouteChangeComplete = (url: string): void => {
|
|
49
|
+
trackPageview({
|
|
50
|
+
url: window.location.origin + url,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
router.events.on('routeChangeComplete', handleRouteChangeComplete)
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
router.events.off('routeChangeComplete', handleRouteChangeComplete)
|
|
58
|
+
}
|
|
59
|
+
}, [router.events, trackPageview, client, disableAutoTrack])
|
|
60
|
+
|
|
61
|
+
// Track initial pageview (routeChangeComplete doesn't fire on initial load)
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (
|
|
64
|
+
!trackPageview ||
|
|
65
|
+
!client ||
|
|
66
|
+
disableAutoTrack ||
|
|
67
|
+
!router.isReady ||
|
|
68
|
+
hasTrackedInitialPageview.current
|
|
69
|
+
) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
hasTrackedInitialPageview.current = true
|
|
74
|
+
trackPageview({
|
|
75
|
+
url: window.location.href,
|
|
76
|
+
})
|
|
77
|
+
}, [trackPageview, client, disableAutoTrack, router.isReady])
|
|
78
|
+
|
|
79
|
+
// This component doesn't render anything
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
NextFathomTrackViewPages.displayName = 'NextFathomTrackViewPages'
|