Skip to content

Commit e4761f3

Browse files
authored
Merge pull request #408 from bndby/#368
#368 🧊 [test]: Test for useBluetooth #368
2 parents f06ea0d + 1d519e4 commit e4761f3

File tree

1 file changed

+250
-0
lines changed

1 file changed

+250
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { act, renderHook, waitFor } from '@testing-library/react';
2+
3+
import { createTrigger, renderHookServer } from '@/tests';
4+
5+
import { useBluetooth } from './useBluetooth';
6+
7+
const trigger = createTrigger<string, () => void>();
8+
9+
// Mock BluetoothRemoteGATTServer
10+
const mockGattServer = {
11+
connected: true,
12+
connect: vi.fn(() => Promise.resolve(mockGattServer)),
13+
disconnect: vi.fn()
14+
};
15+
16+
// Mock BluetoothDevice
17+
const mockBluetoothDevice = {
18+
id: 'test-device-id',
19+
name: 'Test Device',
20+
gatt: mockGattServer,
21+
addEventListener: (type: string, callback: () => void) => trigger.add(type, callback),
22+
removeEventListener: (type: string, callback: () => void) => {
23+
if (trigger.get(type) === callback) trigger.delete(type);
24+
},
25+
dispatchEvent: (event: Event) => {
26+
trigger.callback(event.type);
27+
return true;
28+
}
29+
};
30+
31+
// Mock navigator.bluetooth
32+
const mockNavigatorBluetooth = {
33+
requestDevice: vi.fn(() => Promise.resolve(mockBluetoothDevice))
34+
};
35+
36+
beforeEach(() => {
37+
Object.defineProperty(navigator, 'bluetooth', {
38+
value: mockNavigatorBluetooth,
39+
writable: true,
40+
configurable: true
41+
});
42+
43+
vi.clearAllMocks();
44+
trigger.clear();
45+
mockGattServer.connected = true;
46+
});
47+
48+
it('Should initialize with correct default values', () => {
49+
const { result } = renderHook(() => useBluetooth());
50+
51+
expect(result.current).toEqual({
52+
supported: true,
53+
connected: false,
54+
device: undefined,
55+
server: undefined,
56+
requestDevice: expect.any(Function)
57+
});
58+
});
59+
60+
it('Should detect when Bluetooth is not supported', () => {
61+
Object.defineProperty(navigator, 'bluetooth', {
62+
value: undefined,
63+
writable: true,
64+
configurable: true
65+
});
66+
67+
const { result } = renderHook(() => useBluetooth());
68+
69+
expect(result.current.supported).toBe(false);
70+
});
71+
72+
it('Should use bluetooth on server side', () => {
73+
const { result } = renderHookServer(() => useBluetooth());
74+
75+
expect(result.current).toEqual({
76+
supported: false,
77+
connected: false,
78+
device: undefined,
79+
server: undefined,
80+
requestDevice: expect.any(Function)
81+
});
82+
});
83+
84+
it('Should request device successfully', async () => {
85+
const { result } = renderHook(() => useBluetooth());
86+
87+
await act(async () => {
88+
await result.current.requestDevice();
89+
});
90+
91+
expect(mockNavigatorBluetooth.requestDevice).toHaveBeenCalledWith({
92+
acceptAllDevices: false,
93+
optionalServices: undefined
94+
});
95+
96+
await waitFor(() => {
97+
expect(result.current.device).toBe(mockBluetoothDevice);
98+
});
99+
});
100+
101+
it('Should not request device when not supported', async () => {
102+
Object.defineProperty(navigator, 'bluetooth', {
103+
value: undefined,
104+
writable: true,
105+
configurable: true
106+
});
107+
108+
const { result } = renderHook(() => useBluetooth());
109+
110+
await act(async () => {
111+
await result.current.requestDevice();
112+
});
113+
114+
expect(mockNavigatorBluetooth.requestDevice).not.toHaveBeenCalled();
115+
});
116+
117+
it('Should connect to GATT server after device selection', async () => {
118+
const { result } = renderHook(() => useBluetooth());
119+
120+
await act(async () => {
121+
await result.current.requestDevice();
122+
});
123+
124+
await waitFor(() => {
125+
expect(mockGattServer.connect).toHaveBeenCalled();
126+
expect(result.current.connected).toBe(true);
127+
expect(result.current.server).toBe(mockGattServer);
128+
});
129+
});
130+
131+
it('Should handle device options correctly', async () => {
132+
const options = {
133+
acceptAllDevices: true,
134+
optionalServices: ['battery_service' as BluetoothServiceUUID]
135+
};
136+
137+
const { result } = renderHook(() => useBluetooth(options));
138+
139+
await act(async () => {
140+
await result.current.requestDevice();
141+
});
142+
143+
expect(mockNavigatorBluetooth.requestDevice).toHaveBeenCalledWith({
144+
acceptAllDevices: true,
145+
optionalServices: ['battery_service']
146+
});
147+
});
148+
149+
it('Should handle filters option correctly', async () => {
150+
const options = {
151+
filters: [{ name: 'Test Device' }],
152+
optionalServices: ['battery_service' as BluetoothServiceUUID]
153+
};
154+
155+
const { result } = renderHook(() => useBluetooth(options));
156+
157+
await act(async () => {
158+
await result.current.requestDevice();
159+
});
160+
161+
expect(mockNavigatorBluetooth.requestDevice).toHaveBeenCalledWith({
162+
acceptAllDevices: false,
163+
optionalServices: ['battery_service'],
164+
filters: [{ name: 'Test Device' }]
165+
});
166+
});
167+
168+
it('Should handle gattserverdisconnected event', async () => {
169+
const { result } = renderHook(() => useBluetooth());
170+
171+
await act(async () => {
172+
await result.current.requestDevice();
173+
});
174+
175+
await waitFor(() => {
176+
expect(result.current.connected).toBe(true);
177+
});
178+
179+
act(() => {
180+
mockBluetoothDevice.dispatchEvent(new Event('gattserverdisconnected'));
181+
});
182+
183+
await waitFor(() => {
184+
expect(result.current.connected).toBe(false);
185+
expect(result.current.device).toBeUndefined();
186+
expect(result.current.server).toBeUndefined();
187+
});
188+
});
189+
190+
it('Should cleanup on unmount', async () => {
191+
const removeEventListenerSpy = vi.spyOn(mockBluetoothDevice, 'removeEventListener');
192+
const disconnectSpy = vi.spyOn(mockGattServer, 'disconnect');
193+
194+
const { result, unmount } = renderHook(() => useBluetooth());
195+
196+
await act(async () => {
197+
await result.current.requestDevice();
198+
});
199+
200+
await waitFor(() => {
201+
expect(result.current.connected).toBe(true);
202+
});
203+
204+
unmount();
205+
206+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
207+
'gattserverdisconnected',
208+
expect.any(Function)
209+
);
210+
expect(disconnectSpy).toHaveBeenCalled();
211+
});
212+
213+
it('Should handle device without GATT interface', async () => {
214+
const deviceWithoutGatt = {
215+
...mockBluetoothDevice,
216+
gatt: undefined
217+
} as any;
218+
219+
mockNavigatorBluetooth.requestDevice.mockResolvedValueOnce(deviceWithoutGatt);
220+
221+
const { result } = renderHook(() => useBluetooth());
222+
223+
await act(async () => {
224+
await result.current.requestDevice();
225+
});
226+
227+
await waitFor(() => {
228+
expect(result.current.device).toBe(deviceWithoutGatt);
229+
expect(result.current.connected).toBe(false);
230+
expect(result.current.server).toBeUndefined();
231+
});
232+
});
233+
234+
it('Should handle device request error', async () => {
235+
const requestError = new Error('Device request failed');
236+
mockNavigatorBluetooth.requestDevice.mockRejectedValueOnce(requestError);
237+
238+
const { result } = renderHook(() => useBluetooth());
239+
240+
await expect(async () => {
241+
await act(async () => {
242+
await result.current.requestDevice();
243+
});
244+
}).rejects.toThrow('Device request failed');
245+
246+
// Device should not be set when request fails
247+
expect(result.current.device).toBeUndefined();
248+
expect(result.current.connected).toBe(false);
249+
expect(result.current.server).toBeUndefined();
250+
});

0 commit comments

Comments
 (0)