preact-missing-hooks 4.7.0 → 4.9.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.
@@ -0,0 +1,316 @@
1
+ /** @jsx h */
2
+ import { h } from 'preact'
3
+ import { render, waitFor } from '@testing-library/preact'
4
+ import { getDeviceData, parseUserAgent, useDeviceData } from '../src/useDeviceData'
5
+
6
+ const CHROME_WIN_UA =
7
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
8
+
9
+ describe('parseUserAgent', () => {
10
+ it('parses Chrome on Windows', () => {
11
+ const parsed = parseUserAgent(CHROME_WIN_UA)
12
+ expect(parsed.browser).toEqual({ name: 'Chrome', version: '120.0.0.0' })
13
+ expect(parsed.os).toEqual({ name: 'Windows', version: '10.0' })
14
+ })
15
+
16
+ it('parses Firefox on macOS', () => {
17
+ const ua =
18
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0'
19
+ const parsed = parseUserAgent(ua)
20
+ expect(parsed.browser.name).toBe('Firefox')
21
+ expect(parsed.browser.version).toBe('121.0')
22
+ expect(parsed.os.name).toBe('macOS')
23
+ expect(parsed.os.version).toBe('10.15')
24
+ })
25
+
26
+ it('parses Safari on iOS', () => {
27
+ const ua =
28
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1'
29
+ const parsed = parseUserAgent(ua)
30
+ expect(parsed.browser.name).toBe('Safari')
31
+ expect(parsed.browser.version).toBe('17.2')
32
+ expect(parsed.os.name).toBe('iOS')
33
+ expect(parsed.os.version).toBe('17.2')
34
+ })
35
+
36
+ it('parses Edge from user-agent', () => {
37
+ const ua =
38
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0'
39
+ const parsed = parseUserAgent(ua)
40
+ expect(parsed.browser.name).toBe('Edge')
41
+ expect(parsed.browser.version).toBe('120.0.0.0')
42
+ })
43
+ })
44
+
45
+ describe('getDeviceData', () => {
46
+ const originalNavigator = global.navigator
47
+ const originalScreen = global.screen
48
+ const originalWindow = global.window
49
+
50
+ afterEach(() => {
51
+ Object.defineProperty(global, 'navigator', {
52
+ value: originalNavigator,
53
+ writable: true,
54
+ })
55
+ Object.defineProperty(global, 'screen', {
56
+ value: originalScreen,
57
+ writable: true,
58
+ })
59
+ global.window = originalWindow
60
+ })
61
+
62
+ it('returns SSR-safe defaults when navigator is undefined', () => {
63
+ vi.stubGlobal('navigator', undefined)
64
+ const data = getDeviceData()
65
+ vi.unstubAllGlobals()
66
+ expect(data.userAgent).toBe('')
67
+ expect(data.browser).toEqual({ name: 'Unknown', version: '' })
68
+ expect(data.os).toEqual({ name: 'Unknown', version: '' })
69
+ expect(data.online).toBe(true)
70
+ expect(data.viewport.width).toBe(0)
71
+ })
72
+
73
+ it('reads navigator, browser, OS, and screen fields', () => {
74
+ Object.defineProperty(global, 'navigator', {
75
+ value: {
76
+ userAgent: CHROME_WIN_UA,
77
+ language: 'en-US',
78
+ languages: ['en-US', 'en'],
79
+ platform: 'Win32',
80
+ cookieEnabled: true,
81
+ onLine: true,
82
+ hardwareConcurrency: 8,
83
+ deviceMemory: 8,
84
+ maxTouchPoints: 0,
85
+ vendor: 'TestVendor',
86
+ },
87
+ writable: true,
88
+ })
89
+ Object.defineProperty(global, 'screen', {
90
+ value: {
91
+ width: 1920,
92
+ height: 1080,
93
+ availWidth: 1920,
94
+ availHeight: 1040,
95
+ colorDepth: 24,
96
+ },
97
+ writable: true,
98
+ })
99
+
100
+ const data = getDeviceData()
101
+ expect(data.userAgent).toBe(CHROME_WIN_UA)
102
+ expect(data.browser.name).toBe('Chrome')
103
+ expect(data.browser.version).toBe('120.0.0.0')
104
+ expect(data.os.name).toBe('Windows')
105
+ expect(data.os.version).toBe('10.0')
106
+ expect(data.language).toBe('en-US')
107
+ expect(data.platform).toBe('Win32')
108
+ expect(data.hardwareConcurrency).toBe(8)
109
+ expect(data.screen.width).toBe(1920)
110
+ expect(data.touch).toBe(false)
111
+ })
112
+
113
+ it('prefers Client Hints brands over user-agent for browser name', () => {
114
+ Object.defineProperty(global, 'navigator', {
115
+ value: {
116
+ ...originalNavigator,
117
+ userAgent:
118
+ 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/120.0.0.0',
119
+ userAgentData: {
120
+ mobile: true,
121
+ platform: 'Android',
122
+ brands: [
123
+ { brand: 'Not A Brand', version: '99' },
124
+ { brand: 'Google Chrome', version: '120.0.0.0' },
125
+ ],
126
+ },
127
+ },
128
+ writable: true,
129
+ })
130
+
131
+ const data = getDeviceData()
132
+ expect(data.userAgentData?.mobile).toBe(true)
133
+ expect(data.browser.name).toBe('Google Chrome')
134
+ expect(data.browser.version).toBe('120.0.0.0')
135
+ expect(data.os.name).toBe('Android')
136
+ })
137
+ })
138
+
139
+ describe('useDeviceData', () => {
140
+ const originalNavigator = global.navigator
141
+ const originalAddEventListener = window.addEventListener
142
+ const originalRemoveEventListener = window.removeEventListener
143
+
144
+ afterEach(() => {
145
+ Object.defineProperty(global, 'navigator', {
146
+ value: originalNavigator,
147
+ writable: true,
148
+ })
149
+ window.addEventListener = originalAddEventListener
150
+ window.removeEventListener = originalRemoveEventListener
151
+ })
152
+
153
+ it('returns browser and OS from navigator user-agent', () => {
154
+ Object.defineProperty(global, 'navigator', {
155
+ value: {
156
+ ...originalNavigator,
157
+ userAgent: CHROME_WIN_UA,
158
+ language: 'fr',
159
+ languages: ['fr'],
160
+ platform: 'Win32',
161
+ cookieEnabled: true,
162
+ onLine: true,
163
+ maxTouchPoints: 0,
164
+ vendor: 'Google Inc.',
165
+ },
166
+ writable: true,
167
+ })
168
+
169
+ function TestComponent() {
170
+ const device = useDeviceData({
171
+ includeBattery: false,
172
+ includeHighEntropy: false,
173
+ })
174
+ return (
175
+ <div>
176
+ <span data-testid="browser">
177
+ {device.browser.name}:{device.browser.version}
178
+ </span>
179
+ <span data-testid="os">
180
+ {device.os.name}:{device.os.version}
181
+ </span>
182
+ <span data-testid="lang">{device.language}</span>
183
+ </div>
184
+ )
185
+ }
186
+
187
+ const { getByTestId } = render(<TestComponent />)
188
+ expect(getByTestId('browser').textContent).toBe('Chrome:120.0.0.0')
189
+ expect(getByTestId('os').textContent).toBe('Windows:10.0')
190
+ expect(getByTestId('lang').textContent).toBe('fr')
191
+ })
192
+
193
+ it('enriches browser and OS version from getHighEntropyValues', async () => {
194
+ Object.defineProperty(global, 'navigator', {
195
+ value: {
196
+ ...originalNavigator,
197
+ userAgent: CHROME_WIN_UA,
198
+ userAgentData: {
199
+ platform: 'Windows',
200
+ brands: [{ brand: 'Google Chrome', version: '120.0.0.0' }],
201
+ getHighEntropyValues: vi.fn().mockResolvedValue({
202
+ platformVersion: '15.0.0',
203
+ fullVersionList: [
204
+ { brand: 'Google Chrome', version: '120.0.6099.130' },
205
+ ],
206
+ }),
207
+ },
208
+ },
209
+ writable: true,
210
+ })
211
+
212
+ function TestComponent() {
213
+ const device = useDeviceData({
214
+ includeBattery: false,
215
+ includeHighEntropy: true,
216
+ })
217
+ return (
218
+ <div>
219
+ <span data-testid="browser">
220
+ {device.browser.name}:{device.browser.version}
221
+ </span>
222
+ <span data-testid="os">
223
+ {device.os.name}:{device.os.version}
224
+ </span>
225
+ </div>
226
+ )
227
+ }
228
+
229
+ const { getByTestId } = render(<TestComponent />)
230
+ await waitFor(() => {
231
+ expect(getByTestId('browser').textContent).toBe(
232
+ 'Google Chrome:120.0.6099.130',
233
+ )
234
+ expect(getByTestId('os').textContent).toBe('Windows:15.0.0')
235
+ })
236
+ })
237
+
238
+ it('updates viewport on resize', async () => {
239
+ let resizeHandler: () => void = () => {}
240
+ window.addEventListener = vi.fn((event: string, handler: () => void) => {
241
+ if (event === 'resize') resizeHandler = handler
242
+ }) as typeof window.addEventListener
243
+ window.removeEventListener = vi.fn()
244
+
245
+ Object.defineProperty(window, 'innerWidth', {
246
+ configurable: true,
247
+ value: 800,
248
+ })
249
+ Object.defineProperty(window, 'innerHeight', {
250
+ configurable: true,
251
+ value: 600,
252
+ })
253
+
254
+ Object.defineProperty(global, 'navigator', {
255
+ value: { ...originalNavigator, onLine: true, userAgent: CHROME_WIN_UA },
256
+ writable: true,
257
+ })
258
+
259
+ function TestComponent() {
260
+ const device = useDeviceData({
261
+ includeBattery: false,
262
+ includeHighEntropy: false,
263
+ })
264
+ return <span data-testid="vw">{device.viewport.width}</span>
265
+ }
266
+
267
+ const { getByTestId } = render(<TestComponent />)
268
+ expect(getByTestId('vw').textContent).toBe('800')
269
+
270
+ Object.defineProperty(window, 'innerWidth', {
271
+ configurable: true,
272
+ value: 1024,
273
+ })
274
+ resizeHandler()
275
+
276
+ await waitFor(() => {
277
+ expect(getByTestId('vw').textContent).toBe('1024')
278
+ })
279
+ })
280
+
281
+ it('merges battery data when getBattery resolves', async () => {
282
+ Object.defineProperty(global, 'navigator', {
283
+ value: {
284
+ ...originalNavigator,
285
+ onLine: true,
286
+ userAgent: CHROME_WIN_UA,
287
+ getBattery: () =>
288
+ Promise.resolve({ charging: true, level: 0.75 }),
289
+ },
290
+ writable: true,
291
+ })
292
+
293
+ function TestComponent() {
294
+ const device = useDeviceData({
295
+ includeBattery: true,
296
+ includeHighEntropy: false,
297
+ batteryPollIntervalMs: 0,
298
+ })
299
+ return (
300
+ <span data-testid="battery">
301
+ {device.battery
302
+ ? `${device.battery.charging}-${device.battery.level}`
303
+ : 'none'}
304
+ </span>
305
+ )
306
+ }
307
+
308
+ const { getByTestId } = render(<TestComponent />)
309
+ await waitFor(
310
+ () => {
311
+ expect(getByTestId('battery').textContent).toBe('true-0.75')
312
+ },
313
+ { timeout: 3000 },
314
+ )
315
+ })
316
+ })