use-page-view 0.0.1 → 1.0.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 +173 -101
- package/dist/__tests__/use-page-view.test.js +1 -1
- package/package.json +21 -7
package/README.md
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
# use-page-view
|
2
2
|
|
3
|
-
A React hook for tracking page views and user engagement time. This hook provides real-time
|
3
|
+
A React hook for tracking page views and user engagement time. This hook provides real-time tracking of how long users spend on a page, their activity status, and flexible callbacks for implementing custom persistence and analytics.
|
4
4
|
|
5
5
|
[](https://www.npmjs.com/package/use-page-view)
|
6
6
|
[](https://www.npmjs.com/package/use-page-view)
|
7
|
+
[](https://bundlephobia.com/package/use-page-view)
|
7
8
|
[](https://github.com/christophersesugh/use-page-view/blob/main/LICENSE)
|
8
9
|
[](https://github.com/christophersesugh/use-page-view/actions)
|
9
10
|
[](https://codecov.io/gh/christophersesugh/use-page-view)
|
@@ -12,10 +13,13 @@ A React hook for tracking page views and user engagement time. This hook provide
|
|
12
13
|
|
13
14
|
- 📊 Track time spent on pages
|
14
15
|
- 👀 Monitor user activity and page visibility
|
15
|
-
-
|
16
|
-
-
|
17
|
-
- 🎯 Support for
|
18
|
-
-
|
16
|
+
- 🔄 Periodic updates through callback
|
17
|
+
- ⏱️ Configurable thresholds and intervals
|
18
|
+
- 🎯 Support for one-time tracking
|
19
|
+
- 🔑 User identification support
|
20
|
+
- 🚀 Lightweight with minimal overhead
|
21
|
+
- 🌐 SSR/React Router/Next.js compatible
|
22
|
+
- 🛠️ Flexible - implement your own persistence strategy
|
19
23
|
|
20
24
|
## Installation
|
21
25
|
|
@@ -27,7 +31,37 @@ yarn add use-page-view
|
|
27
31
|
pnpm add use-page-view
|
28
32
|
```
|
29
33
|
|
30
|
-
##
|
34
|
+
## Quick Start
|
35
|
+
|
36
|
+
```tsx
|
37
|
+
import { usePageView } from 'use-page-view';
|
38
|
+
|
39
|
+
function MyPage() {
|
40
|
+
const { timeSpent, isActive } = usePageView({
|
41
|
+
pageId: 'my-page',
|
42
|
+
onPageView: (data) => console.log('Page view:', data),
|
43
|
+
});
|
44
|
+
|
45
|
+
return (
|
46
|
+
<div>
|
47
|
+
<h1>My Page</h1>
|
48
|
+
<p>
|
49
|
+
Time spent: {timeSpent}s {isActive ? '🟢 Active' : '🔴 Inactive'}
|
50
|
+
</p>
|
51
|
+
</div>
|
52
|
+
);
|
53
|
+
}
|
54
|
+
```
|
55
|
+
|
56
|
+
## Compatibility
|
57
|
+
|
58
|
+
- **React**: 16.8+ (hooks support required)
|
59
|
+
- **TypeScript**: Full TypeScript support included
|
60
|
+
- **Browsers**: Modern browsers
|
61
|
+
- **SSR**: Compatible with React Router, Next.js and other SSR frameworks
|
62
|
+
- **React Native**: Not supported (web-only)
|
63
|
+
|
64
|
+
## Usage
|
31
65
|
|
32
66
|
```tsx
|
33
67
|
import { usePageView } from 'use-page-view';
|
@@ -35,174 +69,212 @@ import { usePageView } from 'use-page-view';
|
|
35
69
|
function BlogPost() {
|
36
70
|
const { timeSpent, isActive } = usePageView({
|
37
71
|
pageId: 'blog-post-123',
|
38
|
-
|
39
|
-
|
40
|
-
|
72
|
+
userId: 'user-456', // Optional
|
73
|
+
minTimeThreshold: 10, // Minimum time before recording (seconds)
|
74
|
+
heartbeatInterval: 30, // How often to send updates (seconds)
|
75
|
+
inactivityThreshold: 60, // Time before user is considered inactive (seconds)
|
76
|
+
onPageView: async (data) => {
|
77
|
+
try {
|
78
|
+
await fetch('/api/track-page-view', {
|
79
|
+
method: 'POST',
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
81
|
+
body: JSON.stringify(data),
|
82
|
+
});
|
83
|
+
} catch (error) {
|
84
|
+
console.error('Failed to track page view:', error);
|
85
|
+
}
|
41
86
|
},
|
42
87
|
});
|
43
88
|
|
44
89
|
return (
|
45
90
|
<div>
|
46
91
|
<div>
|
47
|
-
Time
|
92
|
+
Time: {formatTime(timeSpent)} {isActive ? '🟢' : '🔴'}
|
48
93
|
</div>
|
49
94
|
<article>Your content here...</article>
|
50
95
|
</div>
|
51
96
|
);
|
52
97
|
}
|
53
|
-
```
|
54
98
|
|
55
|
-
|
99
|
+
// Helper function to format time
|
100
|
+
function formatTime(seconds: number): string {
|
101
|
+
const mins = Math.floor(seconds / 60);
|
102
|
+
const secs = seconds % 60;
|
103
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
104
|
+
}
|
105
|
+
```
|
56
106
|
|
57
|
-
|
107
|
+
## API
|
58
108
|
|
59
|
-
|
109
|
+
### usePageView(options)
|
60
110
|
|
61
|
-
|
62
|
-
| --------------------- | ------------------------------ | -------- | ------- | ---------------------------------------------------------------------- |
|
63
|
-
| `pageId` | `string` | Yes | - | Unique identifier for the page being tracked |
|
64
|
-
| `userId` | `string` | No | - | Optional user identifier if user is logged in |
|
65
|
-
| `minTimeThreshold` | `number` | No | `5` | Minimum time in seconds before recording a view |
|
66
|
-
| `heartbeatInterval` | `number` | No | `30` | How often to send updates in seconds |
|
67
|
-
| `inactivityThreshold` | `number` | No | `30` | Time in seconds before user is considered inactive |
|
68
|
-
| `onPageView` | `(data: PageViewData) => void` | No | - | Callback function to handle page view data |
|
69
|
-
| `trackOnce` | `boolean` | No | `false` | Track only the initial view |
|
70
|
-
| `trackOnceDelay` | `number` | No | `0` | Minimum time in seconds before recording a view when trackOnce is true |
|
111
|
+
#### Options
|
71
112
|
|
72
|
-
|
113
|
+
| Option | Type | Default | Description |
|
114
|
+
| --------------------- | ------------------------------ | -------- | ---------------------------------------------------------------------- |
|
115
|
+
| `pageId` | `string` | Required | Unique identifier for the page being tracked |
|
116
|
+
| `userId` | `string` | Optional | User identifier if user is logged in |
|
117
|
+
| `minTimeThreshold` | `number` | `5` | Minimum time in seconds before recording a view |
|
118
|
+
| `heartbeatInterval` | `number` | `30` | How often to send updates in seconds |
|
119
|
+
| `inactivityThreshold` | `number` | `30` | Time in seconds before user is considered inactive |
|
120
|
+
| `onPageView` | `(data: PageViewData) => void` | Optional | Callback function to handle page view data |
|
121
|
+
| `trackOnce` | `boolean` | `false` | Track only the initial view |
|
122
|
+
| `trackOnceDelay` | `number` | `0` | Minimum time in seconds before recording a view when trackOnce is true |
|
73
123
|
|
74
|
-
|
124
|
+
#### Returns
|
75
125
|
|
76
126
|
| Property | Type | Description |
|
77
127
|
| ----------- | --------- | ------------------------------------------------ |
|
78
128
|
| `timeSpent` | `number` | Total time spent on the page in seconds |
|
79
129
|
| `isActive` | `boolean` | Whether the user is currently active on the page |
|
80
130
|
|
81
|
-
### PageViewData
|
82
|
-
|
83
|
-
The `onPageView` callback receives a `PageViewData` object with the following structure:
|
131
|
+
### PageViewData
|
84
132
|
|
85
133
|
```typescript
|
86
134
|
interface PageViewData {
|
87
|
-
pageId: string;
|
88
|
-
userId?: string;
|
89
|
-
timeSpent: number;
|
90
|
-
isActive: boolean;
|
135
|
+
pageId: string;
|
136
|
+
userId?: string;
|
137
|
+
timeSpent: number;
|
138
|
+
isActive: boolean;
|
91
139
|
}
|
92
140
|
```
|
93
141
|
|
94
|
-
##
|
95
|
-
|
96
|
-
### Tracking User-Specific Views
|
142
|
+
## Examples
|
97
143
|
|
98
|
-
|
99
|
-
function UserProfile() {
|
100
|
-
const { timeSpent, isActive } = usePageView({
|
101
|
-
pageId: 'user-profile',
|
102
|
-
userId: 'user-123', // Track for specific user
|
103
|
-
onPageView: (data) => {
|
104
|
-
// Send to your analytics service
|
105
|
-
analytics.trackPageView(data);
|
106
|
-
},
|
107
|
-
});
|
108
|
-
}
|
109
|
-
```
|
110
|
-
|
111
|
-
### Custom Tracking Intervals
|
144
|
+
### Basic Usage
|
112
145
|
|
113
146
|
```tsx
|
114
147
|
function Article() {
|
115
148
|
const { timeSpent, isActive } = usePageView({
|
116
|
-
pageId: 'article-
|
117
|
-
minTimeThreshold: 10, // Only track after 10 seconds
|
118
|
-
heartbeatInterval: 60, // Send updates every minute
|
119
|
-
inactivityThreshold: 120, // Consider user inactive after 2 minutes
|
149
|
+
pageId: 'article-789',
|
120
150
|
onPageView: (data) => {
|
121
|
-
|
151
|
+
console.log('Article view:', data);
|
122
152
|
},
|
123
153
|
});
|
154
|
+
|
155
|
+
return (
|
156
|
+
<div>
|
157
|
+
<h1>Article Title</h1>
|
158
|
+
<div>Time spent: {formatTime(timeSpent)}</div>
|
159
|
+
<p>Article content...</p>
|
160
|
+
</div>
|
161
|
+
);
|
124
162
|
}
|
125
163
|
```
|
126
164
|
|
127
|
-
### One-
|
165
|
+
### One-time Tracking
|
128
166
|
|
129
167
|
```tsx
|
130
168
|
function LandingPage() {
|
131
169
|
const { timeSpent, isActive } = usePageView({
|
132
170
|
pageId: 'landing-page',
|
133
|
-
trackOnce: true,
|
134
|
-
trackOnceDelay:
|
171
|
+
trackOnce: true,
|
172
|
+
trackOnceDelay: 30, // Track after 30 seconds
|
135
173
|
onPageView: (data) => {
|
136
|
-
|
174
|
+
analytics.track('landing_page_view', data);
|
137
175
|
},
|
138
176
|
});
|
177
|
+
|
178
|
+
return (
|
179
|
+
<div>
|
180
|
+
<h1>Welcome</h1>
|
181
|
+
<div>Time on page: {formatTime(timeSpent)}</div>
|
182
|
+
</div>
|
183
|
+
);
|
139
184
|
}
|
140
185
|
```
|
141
186
|
|
142
|
-
###
|
187
|
+
### Error Handling
|
143
188
|
|
144
189
|
```tsx
|
145
|
-
function
|
146
|
-
const mins = Math.floor(seconds / 60);
|
147
|
-
const secs = seconds % 60;
|
148
|
-
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
149
|
-
}
|
150
|
-
|
151
|
-
function BlogPost() {
|
190
|
+
function PageWithErrorHandling() {
|
152
191
|
const { timeSpent, isActive } = usePageView({
|
153
|
-
pageId: '
|
154
|
-
onPageView: (data) => {
|
155
|
-
|
192
|
+
pageId: 'important-page',
|
193
|
+
onPageView: async (data) => {
|
194
|
+
try {
|
195
|
+
const response = await fetch('/api/analytics', {
|
196
|
+
method: 'POST',
|
197
|
+
headers: { 'Content-Type': 'application/json' },
|
198
|
+
body: JSON.stringify(data),
|
199
|
+
});
|
200
|
+
|
201
|
+
if (!response.ok) {
|
202
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
203
|
+
}
|
204
|
+
} catch (error) {
|
205
|
+
console.error('Analytics tracking failed:', error);
|
206
|
+
// Optionally queue for retry or use alternative tracking
|
207
|
+
}
|
156
208
|
},
|
157
209
|
});
|
158
210
|
|
159
|
-
return
|
160
|
-
<div>
|
161
|
-
<div>
|
162
|
-
Time: {formatTime(timeSpent)} {isActive ? '🟢' : '🔴'}
|
163
|
-
</div>
|
164
|
-
</div>
|
165
|
-
);
|
211
|
+
return <div>Your page content</div>;
|
166
212
|
}
|
167
213
|
```
|
168
214
|
|
169
|
-
##
|
215
|
+
## Features in Detail
|
170
216
|
|
171
|
-
|
217
|
+
### Time Tracking
|
172
218
|
|
173
|
-
|
219
|
+
- Tracks total time spent on the page
|
220
|
+
- Updates every second
|
221
|
+
- Handles browser tab switching and page visibility changes
|
222
|
+
- Supports custom initial time values for persistence scenarios
|
174
223
|
|
175
|
-
|
224
|
+
### Activity Monitoring
|
176
225
|
|
177
|
-
|
178
|
-
|
226
|
+
- Detects user activity through mouse, keyboard, and touch events
|
227
|
+
- Monitors page visibility changes
|
228
|
+
- Updates active status based on user engagement
|
229
|
+
- Configurable inactivity threshold
|
179
230
|
|
180
|
-
|
231
|
+
### Flexible Persistence
|
181
232
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
233
|
+
- No built-in persistence to keep the library lightweight
|
234
|
+
- Easy to implement custom persistence strategies
|
235
|
+
|
236
|
+
### Callback System
|
237
|
+
|
238
|
+
- Periodic updates through `onPageView` callback
|
239
|
+
- Configurable update interval with `heartbeatInterval`
|
240
|
+
- Minimum time threshold with `minTimeThreshold`
|
241
|
+
- One-time tracking option with `trackOnce`
|
242
|
+
|
243
|
+
## Performance
|
244
|
+
|
245
|
+
- **Minimal overhead**: Efficient event handling with automatic cleanup
|
246
|
+
- **Memory efficient**: Uses `requestIdleCallback` when available
|
247
|
+
- **Bundle size**: < 5KB minified + gzipped
|
248
|
+
- **No dependencies**: Zero external dependencies for maximum compatibility
|
249
|
+
- **Automatic cleanup**: All event listeners and timers are cleaned up on unmount
|
250
|
+
|
251
|
+
## FAQ
|
252
|
+
|
253
|
+
**Q: Does this work with SSR/React Router/Next.js?**
|
254
|
+
A: Yes, the hook safely handles server-side rendering by checking for browser environment before initializing.
|
255
|
+
|
256
|
+
**Q: How do I persist time tracking across page reloads?**
|
257
|
+
A: Implement your own persistence strategy using the examples above. You can use localStorage, sessionStorage, databases, or any other storage method that fits your needs.
|
258
|
+
|
259
|
+
**Q: How accurate is the time tracking?**
|
260
|
+
A: Time tracking is updated every second and accounts for page visibility changes and user inactivity.
|
192
261
|
|
193
|
-
|
262
|
+
**Q: Can I track multiple pages in the same app?**
|
263
|
+
A: Yes, use different `pageId` values for each page or component you want to track separately.
|
194
264
|
|
195
|
-
|
265
|
+
**Q: Does this affect my app's performance?**
|
266
|
+
A: No, the hook uses efficient event handling and cleanup. The bundle size is minimal (< 3KB).
|
196
267
|
|
197
|
-
|
198
|
-
|
268
|
+
**Q: What events are considered "activity"?**
|
269
|
+
A: Mouse movement, clicks, keyboard input, touch events, and scroll events are all considered user activity.
|
199
270
|
|
200
|
-
|
271
|
+
**Q: Why doesn't the hook include built-in localStorage support?**
|
272
|
+
A: To keep the library lightweight and flexible. Different applications have different persistence needs, and this approach allows you to implement exactly what you need without bloating the core library.
|
201
273
|
|
202
274
|
## Contributing
|
203
275
|
|
204
|
-
|
276
|
+
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
205
277
|
|
206
278
|
## License
|
207
279
|
|
208
|
-
MIT
|
280
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
@@ -179,4 +179,4 @@ Unable to find an `+(r===!1?`accessible `:``)+`element with the role "`+t+`"`+c+
|
|
179
179
|
`+s).trim()},BT=Dw(FT,FT.name,`queryAll`),[VT,HT,UT,WT,GT]=Ow(FT,RT,zT),KT=()=>Q().testIdAttribute,qT=function(){for(var e=arguments.length,t=Array(e),n=0;n<e;n++)t[n]=arguments[n];return xC(t[0]),xw(KT(),...t)},JT=(e,t)=>`Found multiple elements by: [`+KT()+`="`+t+`"]`,YT=(e,t)=>`Unable to find an element by: [`+KT()+`="`+t+`"]`,XT=Dw(qT,qT.name,`queryAll`),[ZT,QT,$T,eE,tE]=Ow(qT,JT,YT);var nE=Object.freeze({__proto__:null,queryAllByLabelText:Vw,queryByLabelText:Fw,getAllByLabelText:zw,getByLabelText:Bw,findAllByLabelText:Lw,findByLabelText:Rw,queryByPlaceholderText:Kw,queryAllByPlaceholderText:Gw,getByPlaceholderText:Jw,getAllByPlaceholderText:qw,findAllByPlaceholderText:Yw,findByPlaceholderText:Xw,queryByText:tT,queryAllByText:eT,getByText:rT,getAllByText:nT,findAllByText:iT,findByText:aT,queryByDisplayValue:uT,queryAllByDisplayValue:lT,getByDisplayValue:fT,getAllByDisplayValue:dT,findAllByDisplayValue:pT,findByDisplayValue:mT,queryByAltText:bT,queryAllByAltText:yT,getByAltText:ST,getAllByAltText:xT,findAllByAltText:CT,findByAltText:wT,queryByTitle:AT,queryAllByTitle:kT,getByTitle:MT,getAllByTitle:jT,findAllByTitle:NT,findByTitle:PT,queryByRole:VT,queryAllByRole:BT,getAllByRole:HT,getByRole:UT,findAllByRole:WT,findByRole:GT,queryByTestId:ZT,queryAllByTestId:XT,getByTestId:$T,getAllByTestId:QT,findAllByTestId:eE,findByTestId:tE});function rE(e,t,n){return t===void 0&&(t=nE),n===void 0&&(n={}),Object.keys(t).reduce((n,r)=>{let i=t[r];return n[r]=i.bind(null,e),n},n)}const iE={copy:{EventType:`ClipboardEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},cut:{EventType:`ClipboardEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},paste:{EventType:`ClipboardEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},compositionEnd:{EventType:`CompositionEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},compositionStart:{EventType:`CompositionEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},compositionUpdate:{EventType:`CompositionEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},keyDown:{EventType:`KeyboardEvent`,defaultInit:{bubbles:!0,cancelable:!0,charCode:0,composed:!0}},keyPress:{EventType:`KeyboardEvent`,defaultInit:{bubbles:!0,cancelable:!0,charCode:0,composed:!0}},keyUp:{EventType:`KeyboardEvent`,defaultInit:{bubbles:!0,cancelable:!0,charCode:0,composed:!0}},focus:{EventType:`FocusEvent`,defaultInit:{bubbles:!1,cancelable:!1,composed:!0}},blur:{EventType:`FocusEvent`,defaultInit:{bubbles:!1,cancelable:!1,composed:!0}},focusIn:{EventType:`FocusEvent`,defaultInit:{bubbles:!0,cancelable:!1,composed:!0}},focusOut:{EventType:`FocusEvent`,defaultInit:{bubbles:!0,cancelable:!1,composed:!0}},change:{EventType:`Event`,defaultInit:{bubbles:!0,cancelable:!1}},input:{EventType:`InputEvent`,defaultInit:{bubbles:!0,cancelable:!1,composed:!0}},invalid:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!0}},submit:{EventType:`Event`,defaultInit:{bubbles:!0,cancelable:!0}},reset:{EventType:`Event`,defaultInit:{bubbles:!0,cancelable:!0}},click:{EventType:`MouseEvent`,defaultInit:{bubbles:!0,cancelable:!0,button:0,composed:!0}},contextMenu:{EventType:`MouseEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},dblClick:{EventType:`MouseEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},drag:{EventType:`DragEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},dragEnd:{EventType:`DragEvent`,defaultInit:{bubbles:!0,cancelable:!1,composed:!0}},dragEnter:{EventType:`DragEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},dragExit:{EventType:`DragEvent`,defaultInit:{bubbles:!0,cancelable:!1,composed:!0}},dragLeave:{EventType:`DragEvent`,defaultInit:{bubbles:!0,cancelable:!1,composed:!0}},dragOver:{EventType:`DragEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},dragStart:{EventType:`DragEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},drop:{EventType:`DragEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},mouseDown:{EventType:`MouseEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},mouseEnter:{EventType:`MouseEvent`,defaultInit:{bubbles:!1,cancelable:!1,composed:!0}},mouseLeave:{EventType:`MouseEvent`,defaultInit:{bubbles:!1,cancelable:!1,composed:!0}},mouseMove:{EventType:`MouseEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},mouseOut:{EventType:`MouseEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},mouseOver:{EventType:`MouseEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},mouseUp:{EventType:`MouseEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},select:{EventType:`Event`,defaultInit:{bubbles:!0,cancelable:!1}},touchCancel:{EventType:`TouchEvent`,defaultInit:{bubbles:!0,cancelable:!1,composed:!0}},touchEnd:{EventType:`TouchEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},touchMove:{EventType:`TouchEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},touchStart:{EventType:`TouchEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},resize:{EventType:`UIEvent`,defaultInit:{bubbles:!1,cancelable:!1}},scroll:{EventType:`UIEvent`,defaultInit:{bubbles:!1,cancelable:!1}},wheel:{EventType:`WheelEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},abort:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},canPlay:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},canPlayThrough:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},durationChange:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},emptied:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},encrypted:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},ended:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},loadedData:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},loadedMetadata:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},loadStart:{EventType:`ProgressEvent`,defaultInit:{bubbles:!1,cancelable:!1}},pause:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},play:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},playing:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},progress:{EventType:`ProgressEvent`,defaultInit:{bubbles:!1,cancelable:!1}},rateChange:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},seeked:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},seeking:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},stalled:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},suspend:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},timeUpdate:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},volumeChange:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},waiting:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},load:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},error:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},animationStart:{EventType:`AnimationEvent`,defaultInit:{bubbles:!0,cancelable:!1}},animationEnd:{EventType:`AnimationEvent`,defaultInit:{bubbles:!0,cancelable:!1}},animationIteration:{EventType:`AnimationEvent`,defaultInit:{bubbles:!0,cancelable:!1}},transitionCancel:{EventType:`TransitionEvent`,defaultInit:{bubbles:!0,cancelable:!1}},transitionEnd:{EventType:`TransitionEvent`,defaultInit:{bubbles:!0,cancelable:!0}},transitionRun:{EventType:`TransitionEvent`,defaultInit:{bubbles:!0,cancelable:!1}},transitionStart:{EventType:`TransitionEvent`,defaultInit:{bubbles:!0,cancelable:!1}},pointerOver:{EventType:`PointerEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},pointerEnter:{EventType:`PointerEvent`,defaultInit:{bubbles:!1,cancelable:!1}},pointerDown:{EventType:`PointerEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},pointerMove:{EventType:`PointerEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},pointerUp:{EventType:`PointerEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},pointerCancel:{EventType:`PointerEvent`,defaultInit:{bubbles:!0,cancelable:!1,composed:!0}},pointerOut:{EventType:`PointerEvent`,defaultInit:{bubbles:!0,cancelable:!0,composed:!0}},pointerLeave:{EventType:`PointerEvent`,defaultInit:{bubbles:!1,cancelable:!1}},gotPointerCapture:{EventType:`PointerEvent`,defaultInit:{bubbles:!0,cancelable:!1,composed:!0}},lostPointerCapture:{EventType:`PointerEvent`,defaultInit:{bubbles:!0,cancelable:!1,composed:!0}},popState:{EventType:`PopStateEvent`,defaultInit:{bubbles:!0,cancelable:!1}},offline:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}},online:{EventType:`Event`,defaultInit:{bubbles:!1,cancelable:!1}}},aE={doubleClick:`dblClick`};function oE(e,t){return Q().eventWrapper(()=>{if(!t)throw Error(`Unable to fire an event - please provide an event object.`);if(!e)throw Error(`Unable to fire a "`+t.type+`" event - please provide a DOM element.`);return e.dispatchEvent(t)})}function sE(e,t,n,r){let{EventType:i=`Event`,defaultInit:a={}}=r===void 0?{}:r;if(!t)throw Error(`Unable to fire a "`+e+`" event - please provide a DOM element.`);let o={...a,...n},{target:{value:s,files:c,...l}={}}=o;s!==void 0&&cE(t,s),c!==void 0&&Object.defineProperty(t,`files`,{configurable:!0,enumerable:!0,writable:!0,value:c}),Object.assign(t,l);let u=bC(t),d=u[i]||u.Event,f;if(typeof d==`function`)f=new d(e,o);else{f=u.document.createEvent(i);let{bubbles:t,cancelable:n,detail:r,...a}=o;f.initEvent(e,t,n,r),Object.keys(a).forEach(e=>{f[e]=a[e]})}let p=[`dataTransfer`,`clipboardData`];return p.forEach(e=>{let t=o[e];typeof t==`object`&&(typeof u.DataTransfer==`function`?Object.defineProperty(f,e,{value:Object.getOwnPropertyNames(t).reduce((e,n)=>(Object.defineProperty(e,n,{value:t[n]}),e),new u.DataTransfer)}):Object.defineProperty(f,e,{value:t}))}),f}Object.keys(iE).forEach(e=>{let{EventType:t,defaultInit:n}=iE[e],r=e.toLowerCase();sE[e]=(e,i)=>sE(r,e,i,{EventType:t,defaultInit:n}),oE[e]=(t,n)=>oE(t,sE[e](t,n))});function cE(e,t){let{set:n}=Object.getOwnPropertyDescriptor(e,`value`)||{},r=Object.getPrototypeOf(e),{set:i}=Object.getOwnPropertyDescriptor(r,`value`)||{};if(i&&n!==i)i.call(e,t);else if(n)n.call(e,t);else throw Error(`The given element does not have a value setter`)}Object.keys(aE).forEach(e=>{let t=aE[e];oE[e]=function(){return oE[t](...arguments)}});function lE(e){return e.replace(/[ \t]*[\n][ \t]*/g,`
|
180
180
|
`)}function uE(e){return qS.default.compressToEncodedURIComponent(lE(e))}function dE(e){return`https://testing-playground.com/#markup=`+uE(e)}const fE=(e,t,n)=>Array.isArray(e)?e.forEach(e=>OC(e,t,n)):OC(e,t,n),pE=function(e){if(e===void 0&&(e=yC().body),!e||!(`innerHTML`in e)){console.log(`The element you're providing isn't a valid DOM element.`);return}if(!e.innerHTML){console.log(`The provided element doesn't have any children.`);return}let t=dE(e.innerHTML);return console.log(`Open this URL in your browser
|
181
181
|
|
182
|
-
`+t),t},mE={debug:fE,logTestingPlaygroundURL:pE},hE=typeof document<`u`&&document.body?rE(document.body,nE,mE):Object.keys(nE).reduce((e,t)=>(e[t]=()=>{throw TypeError(`For queries bound to document.body a global document has to be available... Learn more: https://testing-library.com/s/screen-global-error`)},e),mE),gE=typeof i.act==`function`?i.act:o.act;function _E(){if(typeof globalThis<`u`)return globalThis;if(typeof self<`u`)return self;if(typeof window<`u`)return window;if(typeof global<`u`)return global;throw Error(`unable to locate global object`)}function vE(e){_E().IS_REACT_ACT_ENVIRONMENT=e}function yE(){return _E().IS_REACT_ACT_ENVIRONMENT}function bE(e){return t=>{let n=yE();vE(!0);try{let r=!1,i=e(()=>{let e=t();return typeof e==`object`&&e&&typeof e.then==`function`&&(r=!0),e});if(r){let e=i;return{then:(t,r)=>{e.then(e=>{vE(n),t(e)},e=>{vE(n),r(e)})}}}else return vE(n),i}catch(e){throw vE(n),e}}}const xE=bE(gE),$=function(){return oE(...arguments)};Object.keys(oE).forEach(e=>{$[e]=function(){return oE[e](...arguments)}});const SE=$.mouseEnter,CE=$.mouseLeave;$.mouseEnter=function(){return SE(...arguments),$.mouseOver(...arguments)},$.mouseLeave=function(){return CE(...arguments),$.mouseOut(...arguments)};const wE=$.pointerEnter,TE=$.pointerLeave;$.pointerEnter=function(){return wE(...arguments),$.pointerOver(...arguments)},$.pointerLeave=function(){return TE(...arguments),$.pointerOut(...arguments)};const EE=$.select;$.select=(e,t)=>{EE(e,t),e.focus(),$.keyUp(e,t)};const DE=$.blur,OE=$.focus;$.blur=function(){return $.focusOut(...arguments),DE(...arguments)},$.focus=function(){return $.focusIn(...arguments),OE(...arguments)};let kE={reactStrictMode:!1};function AE(){return{...Q(),...kE}}function jE(){return typeof jest<`u`&&jest!==null?setTimeout._isMockFunction===!0||Object.prototype.hasOwnProperty.call(setTimeout,`clock`):!1}jC({unstable_advanceTimersWrapper:e=>xE(e),asyncWrapper:async e=>{let t=yE();vE(!1);try{let t=await e();return await new Promise(e=>{setTimeout(()=>{e()},0),jE()&&jest.advanceTimersByTime(0)}),t}finally{vE(t)}},eventWrapper:e=>{let t;return xE(()=>{t=e()}),t}});const ME=new Set,NE=[];function PE(e){return AE().reactStrictMode?i.createElement(i.StrictMode,null,e):e}function FE(e,t){return t?i.createElement(t,null,e):e}function IE(e,t){let{hydrate:n,ui:r,wrapper:i}=t,a;return n?xE(()=>{a=c.hydrateRoot(e,PE(FE(r,i)))}):a=c.createRoot(e),{hydrate(){if(!n)throw Error("Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.")},render(e){a.render(e)},unmount(){a.unmount()}}}function LE(e){return{hydrate(t){s.hydrate(t,e)},render(t){s.render(t,e)},unmount(){s.unmountComponentAtNode(e)}}}function RE(e,t){let{baseElement:n,container:r,hydrate:i,queries:a,root:o,wrapper:s}=t;return xE(()=>{i?o.hydrate(PE(FE(e,s)),r):o.render(PE(FE(e,s)),r)}),{container:r,baseElement:n,debug:function(e,t,r){return e===void 0&&(e=n),Array.isArray(e)?e.forEach(e=>console.log(DC(e,t,r))):console.log(DC(e,t,r))},unmount:()=>{xE(()=>{o.unmount()})},rerender:e=>{RE(e,{container:r,baseElement:n,root:o,wrapper:s})},asFragment:()=>{if(typeof document.createRange==`function`)return document.createRange().createContextualFragment(r.innerHTML);{let e=document.createElement(`template`);return e.innerHTML=r.innerHTML,e.content}},...rE(n,a)}}function zE(e,t){let{container:n,baseElement:r=n,legacyRoot:i=!1,queries:a,hydrate:o=!1,wrapper:c}=t===void 0?{}:t;if(i&&typeof s.render!=`function`){let e=Error("`legacyRoot: true` is not supported in this version of React. Please use React 18 instead.");throw Error.captureStackTrace(e,zE),e}r||=document.body,n||=r.appendChild(document.createElement(`div`));let l;if(ME.has(n))NE.forEach(e=>{e.container===n&&(l=e.root)});else{let t=i?LE:IE;l=t(n,{hydrate:o,ui:e,wrapper:c}),NE.push({container:n,root:l}),ME.add(n)}return RE(e,{container:n,baseElement:r,queries:a,hydrate:o,wrapper:c,root:l})}function BE(){NE.forEach(e=>{let{root:t,container:n}=e;xE(()=>{t.unmount()}),n.parentNode===document.body&&document.body.removeChild(n)}),NE.length=0,ME.clear()}function VE(e,t){t===void 0&&(t={});let{initialProps:n,...r}=t;if(r.legacyRoot&&typeof s.render!=`function`){let e=Error("`legacyRoot: true` is not supported in this version of React. Please use React 18 instead.");throw Error.captureStackTrace(e,VE),e}let a=i.createRef();function o(t){let{renderCallbackProps:n}=t,r=e(n);return i.useEffect(()=>{a.current=r}),null}let{rerender:c,unmount:l}=zE(i.createElement(o,{renderCallbackProps:n}),r);function u(e){return c(i.createElement(o,{renderCallbackProps:e}))}return{result:a,rerender:u,unmount:l}}if((typeof process>`u`||!process.env?.RTL_SKIP_AUTO_CLEANUP)&&(typeof afterEach==`function`?afterEach(()=>{BE()}):typeof teardown==`function`&&teardown(()=>{BE()}),typeof beforeAll==`function`&&typeof afterAll==`function`)){let e=yE();beforeAll(()=>{e=yE(),vE(!0)}),afterAll(()=>{vE(e)})}window.IS_REACT_ACT_ENVIRONMENT=!0,ud(`usePageView`,()=>{let e=Hg.fn(),t={pageId:`test-page`,onPageView:e};cf(()=>{Hg.useFakeTimers(),e.mockClear(),Object.defineProperty(document,`hidden`,{configurable:!0,value:!1})}),lf(()=>{Hg.useRealTimers()}),dd(`should initialize with correct default values`,async()=>{let{result:e}=VE(()=>r(t));await a(async()=>{await Promise.resolve()}),wh(e.current.timeSpent).toBe(0),wh(e.current.isActive).toBe(!0)}),dd(`should track time spent correctly`,async()=>{let{result:e}=VE(()=>r(t));await a(async()=>{await Promise.resolve()}),await a(async()=>{Hg.advanceTimersByTime(5e3),await Promise.resolve()}),wh(e.current.timeSpent).toBe(5)}),dd(`should not call onPageView before minTimeThreshold`,async()=>{VE(()=>r({...t,minTimeThreshold:10})),await a(async()=>{await Promise.resolve()}),await a(async()=>{Hg.advanceTimersByTime(5e3),await Promise.resolve()}),wh(e).not.toHaveBeenCalled()}),dd(`should track user activity correctly`,async()=>{let{result:e}=VE(()=>r({...t,inactivityThreshold:5}));await a(async()=>{await Promise.resolve()}),await a(async()=>{document.dispatchEvent(new MouseEvent(`mousemove`)),Hg.advanceTimersByTime(2e3),await Promise.resolve()}),wh(e.current.isActive).toBe(!0),await a(async()=>{Hg.advanceTimersByTime(6e3),await Promise.resolve()}),wh(e.current.isActive).toBe(!1)}),dd(`should handle page visibility changes`,async()=>{let{result:e}=VE(()=>r(t));await a(async()=>{await Promise.resolve()}),await a(async()=>{Object.defineProperty(document,`hidden`,{configurable:!0,value:!0}),document.dispatchEvent(new Event(`visibilitychange`)),await Promise.resolve()}),wh(e.current.isActive).toBe(!1),await a(async()=>{Object.defineProperty(document,`hidden`,{configurable:!0,value:!1}),document.dispatchEvent(new Event(`visibilitychange`)),await Promise.resolve()}),wh(e.current.isActive).toBe(!0)}),dd(`should handle trackOnce option correctly`,async()=>{VE(()=>r({...t,trackOnce:!0,trackOnceDelay:5})),await a(async()=>{await Promise.resolve()}),wh(e).not.toHaveBeenCalled(),await a(async()=>{Hg.advanceTimersByTime(5e3),await Promise.resolve()}),wh(e).toHaveBeenCalledTimes(1),wh(e).toHaveBeenCalledWith({pageId:`test-page`,timeSpent:5,isActive:!0}),await a(async()=>{Hg.advanceTimersByTime(5e3),await Promise.resolve()}),wh(e).toHaveBeenCalledTimes(1)}),dd(`should send final data on unmount`,async()=>{let{unmount:n}=VE(()=>r({...t,minTimeThreshold:5}));await a(async()=>{await Promise.resolve()}),await a(async()=>{Hg.advanceTimersByTime(1e4),await Promise.resolve()}),n(),wh(e).toHaveBeenCalledWith({pageId:`test-page`,timeSpent:10,isActive:!1})})});
|
182
|
+
`+t),t},mE={debug:fE,logTestingPlaygroundURL:pE},hE=typeof document<`u`&&document.body?rE(document.body,nE,mE):Object.keys(nE).reduce((e,t)=>(e[t]=()=>{throw TypeError(`For queries bound to document.body a global document has to be available... Learn more: https://testing-library.com/s/screen-global-error`)},e),mE),gE=typeof i.act==`function`?i.act:o.act;function _E(){if(typeof globalThis<`u`)return globalThis;if(typeof self<`u`)return self;if(typeof window<`u`)return window;if(typeof global<`u`)return global;throw Error(`unable to locate global object`)}function vE(e){_E().IS_REACT_ACT_ENVIRONMENT=e}function yE(){return _E().IS_REACT_ACT_ENVIRONMENT}function bE(e){return t=>{let n=yE();vE(!0);try{let r=!1,i=e(()=>{let e=t();return typeof e==`object`&&e&&typeof e.then==`function`&&(r=!0),e});if(r){let e=i;return{then:(t,r)=>{e.then(e=>{vE(n),t(e)},e=>{vE(n),r(e)})}}}else return vE(n),i}catch(e){throw vE(n),e}}}const xE=bE(gE),$=function(){return oE(...arguments)};Object.keys(oE).forEach(e=>{$[e]=function(){return oE[e](...arguments)}});const SE=$.mouseEnter,CE=$.mouseLeave;$.mouseEnter=function(){return SE(...arguments),$.mouseOver(...arguments)},$.mouseLeave=function(){return CE(...arguments),$.mouseOut(...arguments)};const wE=$.pointerEnter,TE=$.pointerLeave;$.pointerEnter=function(){return wE(...arguments),$.pointerOver(...arguments)},$.pointerLeave=function(){return TE(...arguments),$.pointerOut(...arguments)};const EE=$.select;$.select=(e,t)=>{EE(e,t),e.focus(),$.keyUp(e,t)};const DE=$.blur,OE=$.focus;$.blur=function(){return $.focusOut(...arguments),DE(...arguments)},$.focus=function(){return $.focusIn(...arguments),OE(...arguments)};let kE={reactStrictMode:!1};function AE(){return{...Q(),...kE}}function jE(){return typeof jest<`u`&&jest!==null?setTimeout._isMockFunction===!0||Object.prototype.hasOwnProperty.call(setTimeout,`clock`):!1}jC({unstable_advanceTimersWrapper:e=>xE(e),asyncWrapper:async e=>{let t=yE();vE(!1);try{let t=await e();return await new Promise(e=>{setTimeout(()=>{e()},0),jE()&&jest.advanceTimersByTime(0)}),t}finally{vE(t)}},eventWrapper:e=>{let t;return xE(()=>{t=e()}),t}});const ME=new Set,NE=[];function PE(e){return AE().reactStrictMode?i.createElement(i.StrictMode,null,e):e}function FE(e,t){return t?i.createElement(t,null,e):e}function IE(e,t){let{hydrate:n,ui:r,wrapper:i}=t,a;return n?xE(()=>{a=c.hydrateRoot(e,PE(FE(r,i)))}):a=c.createRoot(e),{hydrate(){if(!n)throw Error("Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.")},render(e){a.render(e)},unmount(){a.unmount()}}}function LE(e){return{hydrate(t){s.hydrate(t,e)},render(t){s.render(t,e)},unmount(){s.unmountComponentAtNode(e)}}}function RE(e,t){let{baseElement:n,container:r,hydrate:i,queries:a,root:o,wrapper:s}=t;return xE(()=>{i?o.hydrate(PE(FE(e,s)),r):o.render(PE(FE(e,s)),r)}),{container:r,baseElement:n,debug:function(e,t,r){return e===void 0&&(e=n),Array.isArray(e)?e.forEach(e=>console.log(DC(e,t,r))):console.log(DC(e,t,r))},unmount:()=>{xE(()=>{o.unmount()})},rerender:e=>{RE(e,{container:r,baseElement:n,root:o,wrapper:s})},asFragment:()=>{if(typeof document.createRange==`function`)return document.createRange().createContextualFragment(r.innerHTML);{let e=document.createElement(`template`);return e.innerHTML=r.innerHTML,e.content}},...rE(n,a)}}function zE(e,t){let{container:n,baseElement:r=n,legacyRoot:i=!1,queries:a,hydrate:o=!1,wrapper:c}=t===void 0?{}:t;if(i&&typeof s.render!=`function`){let e=Error("`legacyRoot: true` is not supported in this version of React. Please use React 18 instead.");throw Error.captureStackTrace(e,zE),e}r||=document.body,n||=r.appendChild(document.createElement(`div`));let l;if(ME.has(n))NE.forEach(e=>{e.container===n&&(l=e.root)});else{let t=i?LE:IE;l=t(n,{hydrate:o,ui:e,wrapper:c}),NE.push({container:n,root:l}),ME.add(n)}return RE(e,{container:n,baseElement:r,queries:a,hydrate:o,wrapper:c,root:l})}function BE(){NE.forEach(e=>{let{root:t,container:n}=e;xE(()=>{t.unmount()}),n.parentNode===document.body&&document.body.removeChild(n)}),NE.length=0,ME.clear()}function VE(e,t){t===void 0&&(t={});let{initialProps:n,...r}=t;if(r.legacyRoot&&typeof s.render!=`function`){let e=Error("`legacyRoot: true` is not supported in this version of React. Please use React 18 instead.");throw Error.captureStackTrace(e,VE),e}let a=i.createRef();function o(t){let{renderCallbackProps:n}=t,r=e(n);return i.useEffect(()=>{a.current=r}),null}let{rerender:c,unmount:l}=zE(i.createElement(o,{renderCallbackProps:n}),r);function u(e){return c(i.createElement(o,{renderCallbackProps:e}))}return{result:a,rerender:u,unmount:l}}if((typeof process>`u`||!process.env?.RTL_SKIP_AUTO_CLEANUP)&&(typeof afterEach==`function`?afterEach(()=>{BE()}):typeof teardown==`function`&&teardown(()=>{BE()}),typeof beforeAll==`function`&&typeof afterAll==`function`)){let e=yE();beforeAll(()=>{e=yE(),vE(!0)}),afterAll(()=>{vE(e)})}window.IS_REACT_ACT_ENVIRONMENT=!0,ud(`usePageView`,()=>{let e=Hg.fn(),t={pageId:`test-page`,onPageView:e};cf(()=>{Hg.useFakeTimers(),e.mockClear(),Object.defineProperty(document,`hidden`,{configurable:!0,value:!1})}),lf(()=>{Hg.useRealTimers()}),dd(`should initialize with correct default values`,async()=>{let{result:e}=VE(()=>r(t));await a(async()=>{await Promise.resolve()}),wh(e.current.timeSpent).toBe(0),wh(e.current.isActive).toBe(!0)}),dd(`should track time spent correctly`,async()=>{let{result:e}=VE(()=>r(t));await a(async()=>{await Promise.resolve()}),await a(async()=>{Hg.advanceTimersByTime(5e3),await Promise.resolve()}),wh(e.current.timeSpent).toBe(5)}),dd(`should not call onPageView before minTimeThreshold`,async()=>{VE(()=>r({...t,minTimeThreshold:10})),await a(async()=>{await Promise.resolve()}),await a(async()=>{Hg.advanceTimersByTime(5e3),await Promise.resolve()}),wh(e).not.toHaveBeenCalled()}),dd(`should call onPageView after minTimeThreshold`,async()=>{VE(()=>r({...t,minTimeThreshold:10,heartbeatInterval:10})),wh(e).not.toHaveBeenCalled(),await a(async()=>{Hg.advanceTimersByTime(1e4),await Promise.resolve()}),await a(async()=>{Hg.advanceTimersByTime(1e4),await Promise.resolve()}),wh(e).toHaveBeenCalledWith({pageId:`test-page`,timeSpent:10,isActive:!0})}),dd(`should track user activity correctly`,async()=>{let{result:e}=VE(()=>r({...t,inactivityThreshold:5}));await a(async()=>{await Promise.resolve()}),await a(async()=>{document.dispatchEvent(new MouseEvent(`mousemove`)),Hg.advanceTimersByTime(2e3),await Promise.resolve()}),wh(e.current.isActive).toBe(!0),await a(async()=>{Hg.advanceTimersByTime(6e3),await Promise.resolve()}),wh(e.current.isActive).toBe(!1)}),dd(`should handle page visibility changes`,async()=>{let{result:e}=VE(()=>r(t));await a(async()=>{await Promise.resolve()}),await a(async()=>{Object.defineProperty(document,`hidden`,{configurable:!0,value:!0}),document.dispatchEvent(new Event(`visibilitychange`)),await Promise.resolve()}),wh(e.current.isActive).toBe(!1),await a(async()=>{Object.defineProperty(document,`hidden`,{configurable:!0,value:!1}),document.dispatchEvent(new Event(`visibilitychange`)),await Promise.resolve()}),wh(e.current.isActive).toBe(!0)}),dd(`should handle trackOnce option correctly`,async()=>{VE(()=>r({...t,trackOnce:!0,trackOnceDelay:5})),await a(async()=>{await Promise.resolve()}),wh(e).not.toHaveBeenCalled(),await a(async()=>{Hg.advanceTimersByTime(5e3),await Promise.resolve()}),wh(e).toHaveBeenCalledTimes(1),wh(e).toHaveBeenCalledWith({pageId:`test-page`,timeSpent:5,isActive:!0}),await a(async()=>{Hg.advanceTimersByTime(5e3),await Promise.resolve()}),wh(e).toHaveBeenCalledTimes(1)}),dd(`should include userId in page view data when provided`,async()=>{VE(()=>r({...t,userId:`test-user`,minTimeThreshold:5,heartbeatInterval:10})),await a(async()=>{await Promise.resolve()}),await a(async()=>{Hg.advanceTimersByTime(1e4),await Promise.resolve()}),await a(async()=>{Hg.advanceTimersByTime(1e4),await Promise.resolve()}),wh(e).toHaveBeenCalledWith({pageId:`test-page`,userId:`test-user`,timeSpent:10,isActive:!0})}),dd(`should handle custom heartbeat interval`,async()=>{VE(()=>r({...t,minTimeThreshold:5,heartbeatInterval:15})),await a(async()=>{await Promise.resolve()}),await a(async()=>{Hg.advanceTimersByTime(5e3),await Promise.resolve()}),await a(async()=>{Hg.advanceTimersByTime(15e3),await Promise.resolve()}),wh(e).toHaveBeenCalledTimes(1),await a(async()=>{Hg.advanceTimersByTime(15e3),await Promise.resolve()}),wh(e).toHaveBeenCalledTimes(2)}),dd(`should send final data on unmount`,async()=>{let{unmount:n}=VE(()=>r({...t,minTimeThreshold:5}));await a(async()=>{await Promise.resolve()}),await a(async()=>{Hg.advanceTimersByTime(1e4),await Promise.resolve()}),n(),wh(e).toHaveBeenCalledWith({pageId:`test-page`,timeSpent:10,isActive:!1})})});
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "use-page-view",
|
3
|
-
"version": "
|
4
|
-
"description": "React hook for tracking page views",
|
3
|
+
"version": "1.0.1",
|
4
|
+
"description": "A React hook for tracking page views and user engagement time. This hook provides real-time tracking of how long users spend on a page, their activity status, and the ability to persist time tracking across page reloads.",
|
5
5
|
"type": "module",
|
6
6
|
"main": "dist/index.js",
|
7
7
|
"files": [
|
@@ -9,8 +9,19 @@
|
|
9
9
|
],
|
10
10
|
"keywords": [
|
11
11
|
"react",
|
12
|
+
"hooks",
|
12
13
|
"page-view",
|
13
|
-
"
|
14
|
+
"analytics",
|
15
|
+
"tracking",
|
16
|
+
"engagement",
|
17
|
+
"time-tracking",
|
18
|
+
"localstorage",
|
19
|
+
"persistence",
|
20
|
+
"typescript",
|
21
|
+
"react-hooks",
|
22
|
+
"user-activity",
|
23
|
+
"page-visibility",
|
24
|
+
"performance-monitoring"
|
14
25
|
],
|
15
26
|
"author": "Christopher S. Aondona <me@codingsimba.com> (https://codingsimba.com)",
|
16
27
|
"repository": {
|
@@ -23,15 +34,17 @@
|
|
23
34
|
},
|
24
35
|
"scripts": {
|
25
36
|
"build": "tsdown",
|
26
|
-
"test": "vitest",
|
37
|
+
"test": "vitest run",
|
38
|
+
"test:watch": "vitest",
|
39
|
+
"test:coverage": "vitest run --coverage",
|
27
40
|
"lint": "tsc",
|
28
41
|
"format": "prettier --write .",
|
29
42
|
"format:check": "prettier . --check",
|
30
43
|
"exports:check": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
|
31
|
-
"release:
|
44
|
+
"release:version": "changeset version",
|
45
|
+
"release:local": "changeset && changeset version && changeset publish",
|
32
46
|
"prepublishOnly": "npm run ci",
|
33
|
-
"
|
34
|
-
"ci": "npm run build && npm run format:check && npm run lint"
|
47
|
+
"ci": "npm run build && npm run format:check && npm run lint && npm run exports:check && npm run test"
|
35
48
|
},
|
36
49
|
"license": "MIT",
|
37
50
|
"dependencies": {
|
@@ -47,6 +60,7 @@
|
|
47
60
|
"@types/react-dom": "^19.1.0",
|
48
61
|
"@vitejs/plugin-react": "^4.5.1",
|
49
62
|
"@vitest/browser": "^3.2.0",
|
63
|
+
"@vitest/coverage-v8": "^3.2.2",
|
50
64
|
"playwright": "^1.52.0",
|
51
65
|
"prettier": "^3.5.3",
|
52
66
|
"tsdown": "^0.12.6",
|