preact-missing-hooks 2.0.0 → 3.0.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 +83 -2
- package/demo/add.wasm +0 -0
- package/demo/index.html +242 -0
- package/demo/main.js +312 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/index.js.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/useWasmCompute.d.ts +39 -0
- package/dist/useWebRTCIP.d.ts +37 -0
- package/package.json +124 -113
- package/src/index.ts +12 -10
- package/src/useWasmCompute.ts +204 -0
- package/src/useWebRTCIP.ts +176 -0
- package/tests/useWasmCompute.test.tsx +240 -0
- package/tests/useWebRTCIP.test.tsx +167 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/** @jsx h */
|
|
2
|
+
import { h } from 'preact';
|
|
3
|
+
import { render, waitFor } from '@testing-library/preact';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { useWasmCompute } from '@/useWasmCompute';
|
|
6
|
+
|
|
7
|
+
describe('useWasmCompute', () => {
|
|
8
|
+
const originalWindow = global.window;
|
|
9
|
+
const originalWorker = global.Worker;
|
|
10
|
+
const originalWebAssembly = global.WebAssembly;
|
|
11
|
+
const originalCreateObjectURL = URL.createObjectURL;
|
|
12
|
+
const originalRevokeObjectURL = URL.revokeObjectURL;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
(global as unknown as { window: typeof originalWindow }).window = originalWindow;
|
|
16
|
+
(global as unknown as { Worker: typeof originalWorker }).Worker = originalWorker;
|
|
17
|
+
(global as unknown as { WebAssembly: typeof originalWebAssembly }).WebAssembly = originalWebAssembly;
|
|
18
|
+
URL.createObjectURL = originalCreateObjectURL;
|
|
19
|
+
URL.revokeObjectURL = originalRevokeObjectURL;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns error when window is undefined (SSR)', async () => {
|
|
23
|
+
const container = document.createElement('div');
|
|
24
|
+
document.body.appendChild(container);
|
|
25
|
+
(global as unknown as { window: undefined }).window = undefined;
|
|
26
|
+
|
|
27
|
+
function TestComponent() {
|
|
28
|
+
const { error, loading, ready } = useWasmCompute({ wasmUrl: '/test.wasm' });
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<span data-testid="error">{error ?? 'none'}</span>
|
|
32
|
+
<span data-testid="loading">{String(loading)}</span>
|
|
33
|
+
<span data-testid="ready">{String(ready)}</span>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
render(<TestComponent />, { container });
|
|
39
|
+
for (let i = 0; i < 50; i++) {
|
|
40
|
+
const loadingEl = container.querySelector('[data-testid="loading"]');
|
|
41
|
+
if (loadingEl?.textContent === 'false') break;
|
|
42
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
43
|
+
}
|
|
44
|
+
const errorEl = container.querySelector('[data-testid="error"]');
|
|
45
|
+
const readyEl = container.querySelector('[data-testid="ready"]');
|
|
46
|
+
expect(errorEl?.textContent).toContain('SSR');
|
|
47
|
+
expect(readyEl?.textContent).toBe('false');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns error when Worker is not supported', async () => {
|
|
51
|
+
(global as unknown as { Worker: undefined }).Worker = undefined;
|
|
52
|
+
|
|
53
|
+
function TestComponent() {
|
|
54
|
+
const { error, loading, ready } = useWasmCompute({ wasmUrl: '/test.wasm' });
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
<span data-testid="error">{error ?? 'none'}</span>
|
|
58
|
+
<span data-testid="loading">{String(loading)}</span>
|
|
59
|
+
<span data-testid="ready">{String(ready)}</span>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { getByTestId } = render(<TestComponent />);
|
|
65
|
+
await waitFor(() => expect(getByTestId('loading').textContent).toBe('false'));
|
|
66
|
+
expect(getByTestId('error').textContent).toContain('Worker');
|
|
67
|
+
expect(getByTestId('ready').textContent).toBe('false');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns error when WebAssembly is not supported', async () => {
|
|
71
|
+
(global as unknown as { Worker: unknown }).Worker = vi.fn();
|
|
72
|
+
(global as unknown as { WebAssembly: undefined }).WebAssembly = undefined;
|
|
73
|
+
|
|
74
|
+
function TestComponent() {
|
|
75
|
+
const { error, loading, ready } = useWasmCompute({ wasmUrl: '/test.wasm' });
|
|
76
|
+
return (
|
|
77
|
+
<div>
|
|
78
|
+
<span data-testid="error">{error ?? 'none'}</span>
|
|
79
|
+
<span data-testid="loading">{String(loading)}</span>
|
|
80
|
+
<span data-testid="ready">{String(ready)}</span>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { getByTestId } = render(<TestComponent />);
|
|
86
|
+
await waitFor(() => expect(getByTestId('loading').textContent).toBe('false'));
|
|
87
|
+
expect(getByTestId('error').textContent).toContain('WebAssembly');
|
|
88
|
+
expect(getByTestId('ready').textContent).toBe('false');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('initializes worker and becomes ready when worker posts ready', async () => {
|
|
92
|
+
URL.createObjectURL = vi.fn(() => 'blob:mock');
|
|
93
|
+
URL.revokeObjectURL = vi.fn();
|
|
94
|
+
|
|
95
|
+
let messageHandler: ((e: MessageEvent) => void) | null = null;
|
|
96
|
+
const postMessageCalls: unknown[] = [];
|
|
97
|
+
|
|
98
|
+
(global as unknown as { Worker: unknown }).Worker = vi.fn().mockImplementation(() => ({
|
|
99
|
+
addEventListener: (_: string, handler: (e: MessageEvent) => void) => {
|
|
100
|
+
messageHandler = handler;
|
|
101
|
+
},
|
|
102
|
+
removeEventListener: vi.fn(),
|
|
103
|
+
postMessage: (data: unknown) => postMessageCalls.push(data),
|
|
104
|
+
terminate: vi.fn(),
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
function TestComponent() {
|
|
108
|
+
const state = useWasmCompute({ wasmUrl: '/add.wasm', exportName: 'add' });
|
|
109
|
+
return (
|
|
110
|
+
<div>
|
|
111
|
+
<span data-testid="ready">{String(state.ready)}</span>
|
|
112
|
+
<span data-testid="loading">{String(state.loading)}</span>
|
|
113
|
+
<span data-testid="result">{state.result ?? 'none'}</span>
|
|
114
|
+
<span data-testid="error">{state.error ?? 'none'}</span>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { getByTestId } = render(<TestComponent />);
|
|
120
|
+
|
|
121
|
+
expect(postMessageCalls.length).toBe(1);
|
|
122
|
+
expect(postMessageCalls[0]).toEqual({
|
|
123
|
+
type: 'init',
|
|
124
|
+
wasmUrl: '/add.wasm',
|
|
125
|
+
exportName: 'add',
|
|
126
|
+
importObject: {},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(getByTestId('ready').textContent).toBe('false');
|
|
130
|
+
expect(getByTestId('loading').textContent).toBe('true');
|
|
131
|
+
|
|
132
|
+
messageHandler!({ data: { type: 'ready' } } as MessageEvent);
|
|
133
|
+
await waitFor(() => expect(getByTestId('ready').textContent).toBe('true'));
|
|
134
|
+
await waitFor(() => expect(getByTestId('loading').textContent).toBe('false'));
|
|
135
|
+
expect(getByTestId('error').textContent).toBe('none');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('compute() posts compute message and updates result when worker responds', async () => {
|
|
139
|
+
URL.createObjectURL = vi.fn(() => 'blob:mock');
|
|
140
|
+
URL.revokeObjectURL = vi.fn();
|
|
141
|
+
|
|
142
|
+
let messageHandler: ((e: MessageEvent) => void) | null = null;
|
|
143
|
+
const postMessageCalls: unknown[] = [];
|
|
144
|
+
|
|
145
|
+
(global as unknown as { Worker: unknown }).Worker = vi.fn().mockImplementation(() => ({
|
|
146
|
+
addEventListener: (_: string, handler: (e: MessageEvent) => void) => {
|
|
147
|
+
messageHandler = handler;
|
|
148
|
+
},
|
|
149
|
+
removeEventListener: vi.fn(),
|
|
150
|
+
postMessage: (data: unknown) => postMessageCalls.push(data),
|
|
151
|
+
terminate: vi.fn(),
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
let computeFn: (input: number) => Promise<number> = () => Promise.resolve(0);
|
|
155
|
+
|
|
156
|
+
function TestComponent() {
|
|
157
|
+
const state = useWasmCompute<number, number>({ wasmUrl: '/add.wasm' });
|
|
158
|
+
computeFn = state.compute;
|
|
159
|
+
return (
|
|
160
|
+
<div>
|
|
161
|
+
<span data-testid="result">{state.result ?? 'none'}</span>
|
|
162
|
+
<span data-testid="loading">{String(state.loading)}</span>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const { getByTestId } = render(<TestComponent />);
|
|
168
|
+
messageHandler!({ data: { type: 'ready' } } as MessageEvent);
|
|
169
|
+
await waitFor(() => expect(getByTestId('loading').textContent).toBe('false'));
|
|
170
|
+
|
|
171
|
+
const resultPromise = computeFn(7);
|
|
172
|
+
await waitFor(() => expect(getByTestId('loading').textContent).toBe('true'));
|
|
173
|
+
expect(postMessageCalls.some((m: unknown) => (m as { type?: string }).type === 'compute' && (m as { input?: number }).input === 7)).toBe(true);
|
|
174
|
+
|
|
175
|
+
messageHandler!({ data: { type: 'result', result: 42 } } as MessageEvent);
|
|
176
|
+
await expect(resultPromise).resolves.toBe(42);
|
|
177
|
+
await waitFor(() => expect(getByTestId('result').textContent).toBe('42'));
|
|
178
|
+
await waitFor(() => expect(getByTestId('loading').textContent).toBe('false'));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('sets error and rejects compute promise when worker posts error', async () => {
|
|
182
|
+
URL.createObjectURL = vi.fn(() => 'blob:mock');
|
|
183
|
+
URL.revokeObjectURL = vi.fn();
|
|
184
|
+
|
|
185
|
+
let messageHandler: ((e: MessageEvent) => void) | null = null;
|
|
186
|
+
|
|
187
|
+
(global as unknown as { Worker: unknown }).Worker = vi.fn().mockImplementation(() => ({
|
|
188
|
+
addEventListener: (_: string, handler: (e: MessageEvent) => void) => {
|
|
189
|
+
messageHandler = handler;
|
|
190
|
+
},
|
|
191
|
+
removeEventListener: vi.fn(),
|
|
192
|
+
postMessage: vi.fn(),
|
|
193
|
+
terminate: vi.fn(),
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
let computeFn: (input: number) => Promise<number> = () => Promise.resolve(0);
|
|
197
|
+
|
|
198
|
+
function TestComponent() {
|
|
199
|
+
const state = useWasmCompute<number, number>({ wasmUrl: '/bad.wasm' });
|
|
200
|
+
computeFn = state.compute;
|
|
201
|
+
return (
|
|
202
|
+
<div>
|
|
203
|
+
<span data-testid="error">{state.error ?? 'none'}</span>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { getByTestId } = render(<TestComponent />);
|
|
209
|
+
messageHandler!({ data: { type: 'ready' } } as MessageEvent);
|
|
210
|
+
await waitFor(() => expect(getByTestId('error').textContent).toBe('none'));
|
|
211
|
+
|
|
212
|
+
const resultPromise = computeFn(1);
|
|
213
|
+
messageHandler!({ data: { type: 'error', error: 'Export "compute" is not a function' } } as MessageEvent);
|
|
214
|
+
await expect(resultPromise).rejects.toThrow('Export "compute" is not a function');
|
|
215
|
+
await waitFor(() => expect(getByTestId('error').textContent).toContain('not a function'));
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('compute() rejects when WASM is not ready yet', async () => {
|
|
219
|
+
URL.createObjectURL = vi.fn(() => 'blob:mock');
|
|
220
|
+
URL.revokeObjectURL = vi.fn();
|
|
221
|
+
|
|
222
|
+
(global as unknown as { Worker: unknown }).Worker = vi.fn().mockImplementation(() => ({
|
|
223
|
+
addEventListener: vi.fn(),
|
|
224
|
+
removeEventListener: vi.fn(),
|
|
225
|
+
postMessage: vi.fn(),
|
|
226
|
+
terminate: vi.fn(),
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
let computeFn: (input: number) => Promise<number> = () => Promise.resolve(0);
|
|
230
|
+
|
|
231
|
+
function TestComponent() {
|
|
232
|
+
const state = useWasmCompute<number, number>({ wasmUrl: '/add.wasm' });
|
|
233
|
+
computeFn = state.compute;
|
|
234
|
+
return <div data-testid="ready">{String(state.ready)}</div>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
render(<TestComponent />);
|
|
238
|
+
await expect(computeFn(1)).rejects.toThrow('WASM not ready');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** @jsx h */
|
|
2
|
+
import { h } from 'preact';
|
|
3
|
+
import { render, waitFor } from '@testing-library/preact';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { useWebRTCIP } from '@/useWebRTCIP';
|
|
6
|
+
|
|
7
|
+
describe('useWebRTCIP', () => {
|
|
8
|
+
const originalRTCPeerConnection = global.RTCPeerConnection;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
if (originalRTCPeerConnection !== undefined) {
|
|
12
|
+
(global as unknown as { RTCPeerConnection: typeof originalRTCPeerConnection }).RTCPeerConnection =
|
|
13
|
+
originalRTCPeerConnection;
|
|
14
|
+
} else {
|
|
15
|
+
delete (global as unknown as { RTCPeerConnection?: unknown }).RTCPeerConnection;
|
|
16
|
+
}
|
|
17
|
+
vi.useRealTimers();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns error when RTCPeerConnection is not available', async () => {
|
|
21
|
+
delete (global as unknown as { RTCPeerConnection?: unknown }).RTCPeerConnection;
|
|
22
|
+
|
|
23
|
+
function TestComponent() {
|
|
24
|
+
const { ips, loading, error } = useWebRTCIP();
|
|
25
|
+
return (
|
|
26
|
+
<div>
|
|
27
|
+
<span data-testid="loading">{String(loading)}</span>
|
|
28
|
+
<span data-testid="error">{error ?? 'none'}</span>
|
|
29
|
+
<span data-testid="ips">{ips.join(',')}</span>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { getByTestId } = render(<TestComponent />);
|
|
35
|
+
await waitFor(() => expect(getByTestId('loading').textContent).toBe('false'));
|
|
36
|
+
expect(getByTestId('error').textContent).toContain('RTCPeerConnection');
|
|
37
|
+
expect(getByTestId('ips').textContent).toBe('');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('starts with loading true and then resolves with mock ICE candidate', async () => {
|
|
41
|
+
vi.useFakeTimers();
|
|
42
|
+
|
|
43
|
+
let onIceCandidate: (e: { candidate: { candidate: string } | null }) => void = () => { };
|
|
44
|
+
const mockPC = {
|
|
45
|
+
close: vi.fn(),
|
|
46
|
+
createDataChannel: vi.fn(),
|
|
47
|
+
setLocalDescription: vi.fn().mockResolvedValue(undefined),
|
|
48
|
+
onicecandidate: null as ((e: { candidate: { candidate: string } | null }) => void) | null,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
(global as unknown as { RTCPeerConnection: unknown }).RTCPeerConnection = vi.fn().mockImplementation(() => ({
|
|
52
|
+
...mockPC,
|
|
53
|
+
get onicecandidate() {
|
|
54
|
+
return this._onicecandidate;
|
|
55
|
+
},
|
|
56
|
+
set onicecandidate(fn) {
|
|
57
|
+
this._onicecandidate = fn;
|
|
58
|
+
onIceCandidate = fn;
|
|
59
|
+
},
|
|
60
|
+
createOffer: vi.fn().mockResolvedValue({ type: 'offer', sdp: '' }),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
let result: { ips: string[]; loading: boolean; error: string | null } = { ips: [], loading: true, error: null };
|
|
64
|
+
function TestComponent() {
|
|
65
|
+
const state = useWebRTCIP({ timeout: 3000 });
|
|
66
|
+
result = state;
|
|
67
|
+
return (
|
|
68
|
+
<div>
|
|
69
|
+
<span data-testid="loading">{String(state.loading)}</span>
|
|
70
|
+
<span data-testid="ips">{state.ips.join(',')}</span>
|
|
71
|
+
<span data-testid="error">{state.error ?? 'none'}</span>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
render(<TestComponent />);
|
|
77
|
+
expect(result.loading).toBe(true);
|
|
78
|
+
|
|
79
|
+
// Simulate ICE candidate with an IPv4
|
|
80
|
+
onIceCandidate({
|
|
81
|
+
candidate: { candidate: 'candidate:1 1 UDP 123 192.168.1.100 456 typ host' },
|
|
82
|
+
});
|
|
83
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
84
|
+
|
|
85
|
+
expect(result.ips).toContain('192.168.1.100');
|
|
86
|
+
|
|
87
|
+
// Timeout should stop gathering
|
|
88
|
+
vi.advanceTimersByTime(3500);
|
|
89
|
+
await waitFor(() => expect(result.loading).toBe(false));
|
|
90
|
+
|
|
91
|
+
expect(mockPC.close).toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('calls onDetect for each new IP without duplicates', async () => {
|
|
95
|
+
vi.useFakeTimers();
|
|
96
|
+
const detected: string[] = [];
|
|
97
|
+
let onIceCandidate: (e: { candidate: { candidate: string } | null }) => void = () => { };
|
|
98
|
+
|
|
99
|
+
(global as unknown as { RTCPeerConnection: unknown }).RTCPeerConnection = vi.fn().mockImplementation(() => ({
|
|
100
|
+
close: vi.fn(),
|
|
101
|
+
createDataChannel: vi.fn(),
|
|
102
|
+
setLocalDescription: vi.fn().mockResolvedValue(undefined),
|
|
103
|
+
_onicecandidate: null,
|
|
104
|
+
get onicecandidate() {
|
|
105
|
+
return this._onicecandidate;
|
|
106
|
+
},
|
|
107
|
+
set onicecandidate(fn) {
|
|
108
|
+
this._onicecandidate = fn;
|
|
109
|
+
onIceCandidate = fn;
|
|
110
|
+
},
|
|
111
|
+
createOffer: vi.fn().mockResolvedValue({ type: 'offer', sdp: '' }),
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
function TestComponent() {
|
|
115
|
+
const { ips } = useWebRTCIP({
|
|
116
|
+
timeout: 5000,
|
|
117
|
+
onDetect: (ip) => detected.push(ip),
|
|
118
|
+
});
|
|
119
|
+
return <span data-testid="ips">{ips.join(',')}</span>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
render(<TestComponent />);
|
|
123
|
+
|
|
124
|
+
onIceCandidate({
|
|
125
|
+
candidate: { candidate: 'candidate:1 1 UDP 0 10.0.0.1 9 typ host' },
|
|
126
|
+
});
|
|
127
|
+
onIceCandidate({
|
|
128
|
+
candidate: { candidate: 'candidate:2 1 UDP 0 10.0.0.1 9 typ host' },
|
|
129
|
+
});
|
|
130
|
+
onIceCandidate({
|
|
131
|
+
candidate: { candidate: 'candidate:3 1 UDP 0 192.168.0.2 9 typ host' },
|
|
132
|
+
});
|
|
133
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
134
|
+
|
|
135
|
+
expect(detected).toEqual(['10.0.0.1', '192.168.0.2']);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('ignores null candidate (end-of-candidates)', async () => {
|
|
139
|
+
vi.useFakeTimers();
|
|
140
|
+
let onIceCandidate: (e: { candidate: unknown }) => void = () => { };
|
|
141
|
+
|
|
142
|
+
(global as unknown as { RTCPeerConnection: unknown }).RTCPeerConnection = vi.fn().mockImplementation(() => ({
|
|
143
|
+
close: vi.fn(),
|
|
144
|
+
createDataChannel: vi.fn(),
|
|
145
|
+
setLocalDescription: vi.fn().mockResolvedValue(undefined),
|
|
146
|
+
get onicecandidate() {
|
|
147
|
+
return this._onicecandidate;
|
|
148
|
+
},
|
|
149
|
+
set onicecandidate(fn) {
|
|
150
|
+
this._onicecandidate = fn;
|
|
151
|
+
onIceCandidate = fn;
|
|
152
|
+
},
|
|
153
|
+
createOffer: vi.fn().mockResolvedValue({ type: 'offer', sdp: '' }),
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
function TestComponent() {
|
|
157
|
+
const { ips } = useWebRTCIP({ timeout: 100 });
|
|
158
|
+
return <span data-testid="ips">{ips.join(',')}</span>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
render(<TestComponent />);
|
|
162
|
+
onIceCandidate({ candidate: null });
|
|
163
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
164
|
+
const el = document.querySelector('[data-testid="ips"]');
|
|
165
|
+
expect(el?.textContent).toBe('');
|
|
166
|
+
});
|
|
167
|
+
});
|