preact-missing-hooks 4.7.0 → 4.8.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/Readme.md +52 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.modern.mjs +1 -1
- package/dist/index.modern.mjs.map +1 -1
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/react.js +192 -0
- package/dist/useDeviceData.d.ts +82 -0
- package/docs/README.md +1 -0
- package/docs/main.js +35 -0
- package/package.json +9 -3
- package/scripts/ensure-microbundle-patch.cjs +46 -0
- package/src/index.ts +1 -0
- package/src/useDeviceData.ts +285 -0
- package/tests/useDeviceData.test.tsx +213 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "preact/hooks";
|
|
2
|
+
|
|
3
|
+
/** Parsed Client Hints from `navigator.userAgentData` when available */
|
|
4
|
+
export interface UserAgentDataInfo {
|
|
5
|
+
mobile: boolean;
|
|
6
|
+
platform: string;
|
|
7
|
+
brands: ReadonlyArray<{ brand: string; version: string }>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Screen and display metrics from `screen` and `window` */
|
|
11
|
+
export interface DeviceScreenInfo {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
availWidth: number;
|
|
15
|
+
availHeight: number;
|
|
16
|
+
colorDepth: number;
|
|
17
|
+
pixelRatio: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Viewport size (`window.innerWidth` / `innerHeight`) */
|
|
21
|
+
export interface DeviceViewportInfo {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Battery status from the Battery Status API when available */
|
|
27
|
+
export interface DeviceBatteryInfo {
|
|
28
|
+
charging: boolean;
|
|
29
|
+
level: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Snapshot of device / browser data from native Navigator and related APIs */
|
|
33
|
+
export interface DeviceData {
|
|
34
|
+
userAgent: string;
|
|
35
|
+
language: string;
|
|
36
|
+
languages: readonly string[];
|
|
37
|
+
platform: string;
|
|
38
|
+
cookieEnabled: boolean;
|
|
39
|
+
online: boolean;
|
|
40
|
+
hardwareConcurrency?: number;
|
|
41
|
+
/** Approximate device RAM in GB (Chrome / some browsers only) */
|
|
42
|
+
deviceMemory?: number;
|
|
43
|
+
maxTouchPoints: number;
|
|
44
|
+
vendor: string;
|
|
45
|
+
touch: boolean;
|
|
46
|
+
screen: DeviceScreenInfo;
|
|
47
|
+
viewport: DeviceViewportInfo;
|
|
48
|
+
userAgentData?: UserAgentDataInfo;
|
|
49
|
+
reducedMotion: boolean;
|
|
50
|
+
colorScheme: "light" | "dark" | "no-preference";
|
|
51
|
+
battery?: DeviceBatteryInfo;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface UseDeviceDataOptions {
|
|
55
|
+
/** Fetch battery info when the Battery Status API exists (default: true) */
|
|
56
|
+
includeBattery?: boolean;
|
|
57
|
+
/** Battery refresh interval in ms (default: 60000) */
|
|
58
|
+
batteryPollIntervalMs?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface NavigatorUAData {
|
|
62
|
+
mobile?: boolean;
|
|
63
|
+
platform?: string;
|
|
64
|
+
brands?: Array<{ brand: string; version: string }>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type NavigatorWithExtras = Navigator & {
|
|
68
|
+
deviceMemory?: number;
|
|
69
|
+
userAgentData?: NavigatorUAData;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const SSR_DEVICE_DATA: DeviceData = {
|
|
73
|
+
userAgent: "",
|
|
74
|
+
language: "en",
|
|
75
|
+
languages: ["en"],
|
|
76
|
+
platform: "",
|
|
77
|
+
cookieEnabled: false,
|
|
78
|
+
online: true,
|
|
79
|
+
maxTouchPoints: 0,
|
|
80
|
+
vendor: "",
|
|
81
|
+
touch: false,
|
|
82
|
+
screen: {
|
|
83
|
+
width: 0,
|
|
84
|
+
height: 0,
|
|
85
|
+
availWidth: 0,
|
|
86
|
+
availHeight: 0,
|
|
87
|
+
colorDepth: 24,
|
|
88
|
+
pixelRatio: 1,
|
|
89
|
+
},
|
|
90
|
+
viewport: { width: 0, height: 0 },
|
|
91
|
+
reducedMotion: false,
|
|
92
|
+
colorScheme: "no-preference",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function getColorScheme(): DeviceData["colorScheme"] {
|
|
96
|
+
if (typeof window === "undefined" || !window.matchMedia) {
|
|
97
|
+
return "no-preference";
|
|
98
|
+
}
|
|
99
|
+
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
100
|
+
return "dark";
|
|
101
|
+
}
|
|
102
|
+
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
103
|
+
return "light";
|
|
104
|
+
}
|
|
105
|
+
return "no-preference";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getReducedMotion(): boolean {
|
|
109
|
+
if (typeof window === "undefined" || !window.matchMedia) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Reads synchronous device / browser data from Navigator, Screen, and matchMedia. */
|
|
116
|
+
export function getDeviceData(): DeviceData {
|
|
117
|
+
if (typeof navigator === "undefined") {
|
|
118
|
+
return SSR_DEVICE_DATA;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const nav = navigator as NavigatorWithExtras;
|
|
122
|
+
const screen =
|
|
123
|
+
typeof globalThis.screen !== "undefined" ? globalThis.screen : null;
|
|
124
|
+
const win =
|
|
125
|
+
typeof globalThis.window !== "undefined" ? globalThis.window : null;
|
|
126
|
+
|
|
127
|
+
const data: DeviceData = {
|
|
128
|
+
userAgent: nav.userAgent ?? "",
|
|
129
|
+
language: nav.language ?? "",
|
|
130
|
+
languages: nav.languages ? [...nav.languages] : [],
|
|
131
|
+
platform: nav.platform ?? "",
|
|
132
|
+
cookieEnabled: Boolean(nav.cookieEnabled),
|
|
133
|
+
online: Boolean(nav.onLine),
|
|
134
|
+
maxTouchPoints: nav.maxTouchPoints ?? 0,
|
|
135
|
+
vendor: nav.vendor ?? "",
|
|
136
|
+
touch: (nav.maxTouchPoints ?? 0) > 0,
|
|
137
|
+
screen: {
|
|
138
|
+
width: screen?.width ?? 0,
|
|
139
|
+
height: screen?.height ?? 0,
|
|
140
|
+
availWidth: screen?.availWidth ?? 0,
|
|
141
|
+
availHeight: screen?.availHeight ?? 0,
|
|
142
|
+
colorDepth: screen?.colorDepth ?? 24,
|
|
143
|
+
pixelRatio: win?.devicePixelRatio ?? 1,
|
|
144
|
+
},
|
|
145
|
+
viewport: {
|
|
146
|
+
width: win?.innerWidth ?? 0,
|
|
147
|
+
height: win?.innerHeight ?? 0,
|
|
148
|
+
},
|
|
149
|
+
reducedMotion: getReducedMotion(),
|
|
150
|
+
colorScheme: getColorScheme(),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (typeof nav.hardwareConcurrency === "number") {
|
|
154
|
+
data.hardwareConcurrency = nav.hardwareConcurrency;
|
|
155
|
+
}
|
|
156
|
+
if (typeof nav.deviceMemory === "number") {
|
|
157
|
+
data.deviceMemory = nav.deviceMemory;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const uaData = nav.userAgentData;
|
|
161
|
+
if (uaData) {
|
|
162
|
+
data.userAgentData = {
|
|
163
|
+
mobile: Boolean(uaData.mobile),
|
|
164
|
+
platform: uaData.platform ?? "",
|
|
165
|
+
brands: uaData.brands ? [...uaData.brands] : [],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return data;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function readBattery(): Promise<DeviceBatteryInfo | undefined> {
|
|
173
|
+
if (typeof navigator === "undefined") return undefined;
|
|
174
|
+
const getBattery = (
|
|
175
|
+
navigator as Navigator & {
|
|
176
|
+
getBattery?: () => Promise<{
|
|
177
|
+
charging: boolean;
|
|
178
|
+
level: number;
|
|
179
|
+
}>;
|
|
180
|
+
}
|
|
181
|
+
).getBattery;
|
|
182
|
+
if (!getBattery) return undefined;
|
|
183
|
+
try {
|
|
184
|
+
const battery = await getBattery.call(navigator);
|
|
185
|
+
return { charging: battery.charging, level: battery.level };
|
|
186
|
+
} catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Extracts device and browser data from native Navigator, Screen, window, and
|
|
193
|
+
* matchMedia APIs. Updates on resize, orientation, online/offline, and
|
|
194
|
+
* prefers-color-scheme / prefers-reduced-motion changes. Optionally polls the
|
|
195
|
+
* Battery Status API when available.
|
|
196
|
+
*
|
|
197
|
+
* @param options - `includeBattery` (default true), `batteryPollIntervalMs` (default 60000)
|
|
198
|
+
* @returns Current {@link DeviceData} snapshot
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```tsx
|
|
202
|
+
* function DevicePanel() {
|
|
203
|
+
* const device = useDeviceData();
|
|
204
|
+
* return (
|
|
205
|
+
* <dl>
|
|
206
|
+
* <dt>Language</dt><dd>{device.language}</dd>
|
|
207
|
+
* <dt>CPUs</dt><dd>{device.hardwareConcurrency ?? '—'}</dd>
|
|
208
|
+
* <dt>Viewport</dt><dd>{device.viewport.width}×{device.viewport.height}</dd>
|
|
209
|
+
* <dt>Theme</dt><dd>{device.colorScheme}</dd>
|
|
210
|
+
* </dl>
|
|
211
|
+
* );
|
|
212
|
+
* }
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
export function useDeviceData(options: UseDeviceDataOptions = {}): DeviceData {
|
|
216
|
+
const { includeBattery = true, batteryPollIntervalMs = 60_000 } = options;
|
|
217
|
+
|
|
218
|
+
const [data, setData] = useState<DeviceData>(() => getDeviceData());
|
|
219
|
+
|
|
220
|
+
const refresh = useCallback(() => {
|
|
221
|
+
setData((prev) => {
|
|
222
|
+
const next = getDeviceData();
|
|
223
|
+
return prev.battery ? { ...next, battery: prev.battery } : next;
|
|
224
|
+
});
|
|
225
|
+
}, []);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (typeof window === "undefined") return;
|
|
229
|
+
|
|
230
|
+
const onResize = () => refresh();
|
|
231
|
+
window.addEventListener("resize", onResize);
|
|
232
|
+
window.addEventListener("orientationchange", onResize);
|
|
233
|
+
window.addEventListener("online", refresh);
|
|
234
|
+
window.addEventListener("offline", refresh);
|
|
235
|
+
|
|
236
|
+
const reducedMotionMq = window.matchMedia?.(
|
|
237
|
+
"(prefers-reduced-motion: reduce)"
|
|
238
|
+
);
|
|
239
|
+
const darkMq = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
240
|
+
const lightMq = window.matchMedia?.("(prefers-color-scheme: light)");
|
|
241
|
+
|
|
242
|
+
const onMediaChange = () => refresh();
|
|
243
|
+
reducedMotionMq?.addEventListener?.("change", onMediaChange);
|
|
244
|
+
darkMq?.addEventListener?.("change", onMediaChange);
|
|
245
|
+
lightMq?.addEventListener?.("change", onMediaChange);
|
|
246
|
+
|
|
247
|
+
return () => {
|
|
248
|
+
window.removeEventListener("resize", onResize);
|
|
249
|
+
window.removeEventListener("orientationchange", onResize);
|
|
250
|
+
window.removeEventListener("online", refresh);
|
|
251
|
+
window.removeEventListener("offline", refresh);
|
|
252
|
+
reducedMotionMq?.removeEventListener?.("change", onMediaChange);
|
|
253
|
+
darkMq?.removeEventListener?.("change", onMediaChange);
|
|
254
|
+
lightMq?.removeEventListener?.("change", onMediaChange);
|
|
255
|
+
};
|
|
256
|
+
}, [refresh]);
|
|
257
|
+
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
if (!includeBattery || typeof navigator === "undefined") return;
|
|
260
|
+
|
|
261
|
+
let cancelled = false;
|
|
262
|
+
let intervalId: ReturnType<typeof setInterval> | undefined;
|
|
263
|
+
|
|
264
|
+
const updateBattery = async () => {
|
|
265
|
+
const battery = await readBattery();
|
|
266
|
+
if (cancelled || battery === undefined) return;
|
|
267
|
+
setData((prev) => ({ ...prev, battery }));
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
void updateBattery();
|
|
271
|
+
if (batteryPollIntervalMs > 0) {
|
|
272
|
+
intervalId = setInterval(
|
|
273
|
+
() => void updateBattery(),
|
|
274
|
+
batteryPollIntervalMs
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return () => {
|
|
279
|
+
cancelled = true;
|
|
280
|
+
if (intervalId !== undefined) clearInterval(intervalId);
|
|
281
|
+
};
|
|
282
|
+
}, [includeBattery, batteryPollIntervalMs]);
|
|
283
|
+
|
|
284
|
+
return data;
|
|
285
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/** @jsx h */
|
|
2
|
+
import { h } from 'preact'
|
|
3
|
+
import { render, waitFor } from '@testing-library/preact'
|
|
4
|
+
import { getDeviceData, useDeviceData } from '../src/useDeviceData'
|
|
5
|
+
|
|
6
|
+
describe('getDeviceData', () => {
|
|
7
|
+
const originalNavigator = global.navigator
|
|
8
|
+
const originalScreen = global.screen
|
|
9
|
+
const originalWindow = global.window
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
Object.defineProperty(global, 'navigator', {
|
|
13
|
+
value: originalNavigator,
|
|
14
|
+
writable: true,
|
|
15
|
+
})
|
|
16
|
+
Object.defineProperty(global, 'screen', {
|
|
17
|
+
value: originalScreen,
|
|
18
|
+
writable: true,
|
|
19
|
+
})
|
|
20
|
+
global.window = originalWindow
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('returns SSR-safe defaults when navigator is undefined', () => {
|
|
24
|
+
vi.stubGlobal('navigator', undefined)
|
|
25
|
+
const data = getDeviceData()
|
|
26
|
+
vi.unstubAllGlobals()
|
|
27
|
+
expect(data.userAgent).toBe('')
|
|
28
|
+
expect(data.online).toBe(true)
|
|
29
|
+
expect(data.viewport.width).toBe(0)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('reads navigator and screen fields', () => {
|
|
33
|
+
Object.defineProperty(global, 'navigator', {
|
|
34
|
+
value: {
|
|
35
|
+
userAgent: 'TestAgent/1.0',
|
|
36
|
+
language: 'en-US',
|
|
37
|
+
languages: ['en-US', 'en'],
|
|
38
|
+
platform: 'Win32',
|
|
39
|
+
cookieEnabled: true,
|
|
40
|
+
onLine: true,
|
|
41
|
+
hardwareConcurrency: 8,
|
|
42
|
+
deviceMemory: 8,
|
|
43
|
+
maxTouchPoints: 0,
|
|
44
|
+
vendor: 'TestVendor',
|
|
45
|
+
},
|
|
46
|
+
writable: true,
|
|
47
|
+
})
|
|
48
|
+
Object.defineProperty(global, 'screen', {
|
|
49
|
+
value: {
|
|
50
|
+
width: 1920,
|
|
51
|
+
height: 1080,
|
|
52
|
+
availWidth: 1920,
|
|
53
|
+
availHeight: 1040,
|
|
54
|
+
colorDepth: 24,
|
|
55
|
+
},
|
|
56
|
+
writable: true,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const data = getDeviceData()
|
|
60
|
+
expect(data.userAgent).toBe('TestAgent/1.0')
|
|
61
|
+
expect(data.language).toBe('en-US')
|
|
62
|
+
expect(data.languages).toEqual(['en-US', 'en'])
|
|
63
|
+
expect(data.platform).toBe('Win32')
|
|
64
|
+
expect(data.hardwareConcurrency).toBe(8)
|
|
65
|
+
expect(data.deviceMemory).toBe(8)
|
|
66
|
+
expect(data.screen.width).toBe(1920)
|
|
67
|
+
expect(data.screen.height).toBe(1080)
|
|
68
|
+
expect(data.touch).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('includes userAgentData when navigator.userAgentData exists', () => {
|
|
72
|
+
Object.defineProperty(global, 'navigator', {
|
|
73
|
+
value: {
|
|
74
|
+
...originalNavigator,
|
|
75
|
+
userAgentData: {
|
|
76
|
+
mobile: true,
|
|
77
|
+
platform: 'Android',
|
|
78
|
+
brands: [{ brand: 'Chromium', version: '120' }],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
writable: true,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const data = getDeviceData()
|
|
85
|
+
expect(data.userAgentData?.mobile).toBe(true)
|
|
86
|
+
expect(data.userAgentData?.platform).toBe('Android')
|
|
87
|
+
expect(data.userAgentData?.brands[0]?.brand).toBe('Chromium')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('useDeviceData', () => {
|
|
92
|
+
const originalNavigator = global.navigator
|
|
93
|
+
const originalAddEventListener = window.addEventListener
|
|
94
|
+
const originalRemoveEventListener = window.removeEventListener
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
Object.defineProperty(global, 'navigator', {
|
|
98
|
+
value: originalNavigator,
|
|
99
|
+
writable: true,
|
|
100
|
+
})
|
|
101
|
+
window.addEventListener = originalAddEventListener
|
|
102
|
+
window.removeEventListener = originalRemoveEventListener
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('returns device data from navigator', () => {
|
|
106
|
+
Object.defineProperty(global, 'navigator', {
|
|
107
|
+
value: {
|
|
108
|
+
...originalNavigator,
|
|
109
|
+
userAgent: 'HookTest/2.0',
|
|
110
|
+
language: 'fr',
|
|
111
|
+
languages: ['fr'],
|
|
112
|
+
platform: 'MacIntel',
|
|
113
|
+
cookieEnabled: true,
|
|
114
|
+
onLine: true,
|
|
115
|
+
maxTouchPoints: 5,
|
|
116
|
+
vendor: 'Apple',
|
|
117
|
+
},
|
|
118
|
+
writable: true,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
function TestComponent() {
|
|
122
|
+
const device = useDeviceData({ includeBattery: false })
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
<span data-testid="ua">{device.userAgent}</span>
|
|
126
|
+
<span data-testid="lang">{device.language}</span>
|
|
127
|
+
<span data-testid="touch">{String(device.touch)}</span>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { getByTestId } = render(<TestComponent />)
|
|
133
|
+
expect(getByTestId('ua').textContent).toBe('HookTest/2.0')
|
|
134
|
+
expect(getByTestId('lang').textContent).toBe('fr')
|
|
135
|
+
expect(getByTestId('touch').textContent).toBe('true')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('updates viewport on resize', async () => {
|
|
139
|
+
let resizeHandler: () => void = () => {}
|
|
140
|
+
window.addEventListener = vi.fn((event: string, handler: () => void) => {
|
|
141
|
+
if (event === 'resize') resizeHandler = handler
|
|
142
|
+
}) as typeof window.addEventListener
|
|
143
|
+
window.removeEventListener = vi.fn()
|
|
144
|
+
|
|
145
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
146
|
+
configurable: true,
|
|
147
|
+
value: 800,
|
|
148
|
+
})
|
|
149
|
+
Object.defineProperty(window, 'innerHeight', {
|
|
150
|
+
configurable: true,
|
|
151
|
+
value: 600,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
Object.defineProperty(global, 'navigator', {
|
|
155
|
+
value: { ...originalNavigator, onLine: true },
|
|
156
|
+
writable: true,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
function TestComponent() {
|
|
160
|
+
const device = useDeviceData({ includeBattery: false })
|
|
161
|
+
return (
|
|
162
|
+
<span data-testid="vw">{device.viewport.width}</span>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { getByTestId } = render(<TestComponent />)
|
|
167
|
+
expect(getByTestId('vw').textContent).toBe('800')
|
|
168
|
+
|
|
169
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
170
|
+
configurable: true,
|
|
171
|
+
value: 1024,
|
|
172
|
+
})
|
|
173
|
+
resizeHandler()
|
|
174
|
+
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(getByTestId('vw').textContent).toBe('1024')
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('merges battery data when getBattery resolves', async () => {
|
|
181
|
+
Object.defineProperty(global, 'navigator', {
|
|
182
|
+
value: {
|
|
183
|
+
...originalNavigator,
|
|
184
|
+
onLine: true,
|
|
185
|
+
getBattery: () =>
|
|
186
|
+
Promise.resolve({ charging: true, level: 0.75 }),
|
|
187
|
+
},
|
|
188
|
+
writable: true,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
function TestComponent() {
|
|
192
|
+
const device = useDeviceData({
|
|
193
|
+
includeBattery: true,
|
|
194
|
+
batteryPollIntervalMs: 0,
|
|
195
|
+
})
|
|
196
|
+
return (
|
|
197
|
+
<span data-testid="battery">
|
|
198
|
+
{device.battery
|
|
199
|
+
? `${device.battery.charging}-${device.battery.level}`
|
|
200
|
+
: 'none'}
|
|
201
|
+
</span>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const { getByTestId } = render(<TestComponent />)
|
|
206
|
+
await waitFor(
|
|
207
|
+
() => {
|
|
208
|
+
expect(getByTestId('battery').textContent).toBe('true-0.75')
|
|
209
|
+
},
|
|
210
|
+
{ timeout: 3000 },
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
})
|