@gjsify/webrtc 0.4.0 → 0.4.4
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/package.json +73 -70
- package/src/get-user-media.ts +0 -131
- package/src/gst-enum-maps.ts +0 -125
- package/src/gst-init.ts +0 -49
- package/src/gst-stats-parser.ts +0 -137
- package/src/gst-utils.ts +0 -41
- package/src/index.ts +0 -104
- package/src/internal/gst-types.ts +0 -122
- package/src/media-device-info.ts +0 -33
- package/src/media-devices.ts +0 -191
- package/src/media-stream-track.ts +0 -159
- package/src/media-stream.ts +0 -96
- package/src/register/data-channel.ts +0 -11
- package/src/register/error.ts +0 -11
- package/src/register/media-devices.ts +0 -10
- package/src/register/media.ts +0 -15
- package/src/register/peer-connection.ts +0 -20
- package/src/register.spec.ts +0 -55
- package/src/register.ts +0 -10
- package/src/rtc-certificate.ts +0 -110
- package/src/rtc-data-channel.ts +0 -283
- package/src/rtc-dtls-transport.ts +0 -48
- package/src/rtc-dtmf-sender.ts +0 -146
- package/src/rtc-error.ts +0 -49
- package/src/rtc-events.ts +0 -64
- package/src/rtc-ice-candidate.ts +0 -115
- package/src/rtc-ice-transport.ts +0 -104
- package/src/rtc-peer-connection.ts +0 -1039
- package/src/rtc-rtp-receiver.ts +0 -122
- package/src/rtc-rtp-sender.ts +0 -471
- package/src/rtc-rtp-transceiver.ts +0 -131
- package/src/rtc-sctp-transport.ts +0 -48
- package/src/rtc-session-description.ts +0 -64
- package/src/rtc-stats-report.ts +0 -39
- package/src/rtc-track-event.ts +0 -45
- package/src/rtp-capabilities.ts +0 -48
- package/src/tee-multiplexer.ts +0 -75
- package/src/test.mts +0 -11
- package/src/webrtc.spec.ts +0 -1186
- package/src/wpt-helpers.ts +0 -156
- package/src/wpt-media.spec.ts +0 -1154
- package/src/wpt.spec.ts +0 -1136
- package/tsconfig.json +0 -36
- package/tsconfig.tsbuildinfo +0 -1
package/src/wpt.spec.ts
DELETED
|
@@ -1,1136 +0,0 @@
|
|
|
1
|
-
// WPT-ported tests for @gjsify/webrtc.
|
|
2
|
-
//
|
|
3
|
-
// Ported from refs/wpt/webrtc/* (W3C, BSD-3-Clause). Each test group is
|
|
4
|
-
// labeled with the originating WPT file. Ports cover the spec-conformance
|
|
5
|
-
// edges that hand-written tests are prone to missing (range validation,
|
|
6
|
-
// legal vs. illegal enum values, binaryType coercion behaviour, bufferedAmount
|
|
7
|
-
// increment semantics, createDataChannel option constraints).
|
|
8
|
-
//
|
|
9
|
-
// Browser reference: the same WPT files also run in Firefox / Chrome / Safari,
|
|
10
|
-
// so the assertions here are what Mozilla / Google / Apple agree is
|
|
11
|
-
// spec-compliant behaviour.
|
|
12
|
-
|
|
13
|
-
import Gst from 'gi://Gst?version=1.0';
|
|
14
|
-
import { describe, it, expect } from '@gjsify/unit';
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
RTCPeerConnection,
|
|
18
|
-
RTCDataChannel,
|
|
19
|
-
RTCDataChannelEvent,
|
|
20
|
-
RTCPeerConnectionIceEvent,
|
|
21
|
-
RTCSessionDescription,
|
|
22
|
-
RTCIceCandidate,
|
|
23
|
-
RTCError,
|
|
24
|
-
RTCErrorEvent,
|
|
25
|
-
} from './index.js';
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
createDataChannelPair,
|
|
29
|
-
awaitMessage,
|
|
30
|
-
closePeerConnections,
|
|
31
|
-
} from './wpt-helpers.js';
|
|
32
|
-
|
|
33
|
-
Gst.init(null);
|
|
34
|
-
const pipelineReady = Boolean(
|
|
35
|
-
Gst.ElementFactory.find('webrtcbin') && Gst.ElementFactory.find('nicesrc'),
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
/** Wrap a WPT-style `promise_test` body in a timeout. */
|
|
39
|
-
function withTimeout<T>(ms: number, promise: Promise<T>, label: string): Promise<T> {
|
|
40
|
-
return Promise.race([
|
|
41
|
-
promise,
|
|
42
|
-
new Promise<T>((_, reject) =>
|
|
43
|
-
setTimeout(() => reject(new Error(`WPT timeout after ${ms}ms: ${label}`)), ms),
|
|
44
|
-
),
|
|
45
|
-
]);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export default async () => {
|
|
49
|
-
await describe('WPT — RTCDataChannelEvent-constructor.html', async () => {
|
|
50
|
-
// Ported: refs/wpt/webrtc/RTCDataChannelEvent-constructor.html
|
|
51
|
-
//
|
|
52
|
-
// The W3C-spec `assert_equals(RTCDataChannelEvent.length, 2)` check
|
|
53
|
-
// is skipped because SpiderMonkey doesn't expose the internal
|
|
54
|
-
// Function.length for user-defined classes the way Blink / Gecko do.
|
|
55
|
-
|
|
56
|
-
await it('throws TypeError with no init dict', async () => {
|
|
57
|
-
expect(() => new (RTCDataChannelEvent as any)('type')).toThrow();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
await it('throws TypeError with channel: null', async () => {
|
|
61
|
-
expect(() => new RTCDataChannelEvent('type', { channel: null as any })).toThrow();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
await it('throws TypeError with channel: undefined', async () => {
|
|
65
|
-
expect(() => new RTCDataChannelEvent('type', { channel: undefined as any })).toThrow();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (pipelineReady) {
|
|
69
|
-
await it('constructs with a real RTCDataChannel', async () => {
|
|
70
|
-
const pc = new RTCPeerConnection();
|
|
71
|
-
const dc = pc.createDataChannel('wpt');
|
|
72
|
-
const event = new RTCDataChannelEvent('type', { channel: dc });
|
|
73
|
-
expect(event instanceof RTCDataChannelEvent).toBeTruthy();
|
|
74
|
-
expect(event.channel).toBe(dc);
|
|
75
|
-
pc.close();
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
await describe('WPT — RTCDataChannel-binaryType.window.js', async () => {
|
|
81
|
-
// Ported: refs/wpt/webrtc/RTCDataChannel-binaryType.window.js
|
|
82
|
-
//
|
|
83
|
-
// The WPT default is 'arraybuffer' (spec §6.2 — "The initial value
|
|
84
|
-
// is 'arraybuffer'"). Common confusion: the WebSocket spec defaults
|
|
85
|
-
// to 'blob'; RTCDataChannel does NOT.
|
|
86
|
-
//
|
|
87
|
-
// Key spec rule (asserted by WPT): setting an INVALID binaryType
|
|
88
|
-
// value should be silently ignored, keeping the previous value — NOT
|
|
89
|
-
// throw a TypeError. @gjsify/webrtc currently throws; the
|
|
90
|
-
// `invalid binaryType` cases below are therefore marked as known
|
|
91
|
-
// spec deviations.
|
|
92
|
-
|
|
93
|
-
if (!pipelineReady) {
|
|
94
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
95
|
-
expect(pipelineReady).toBeFalsy();
|
|
96
|
-
});
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
await it("default binaryType is 'arraybuffer'", async () => {
|
|
101
|
-
const pc = new RTCPeerConnection();
|
|
102
|
-
const dc = pc.createDataChannel('wpt');
|
|
103
|
-
expect(dc.binaryType).toBe('arraybuffer');
|
|
104
|
-
pc.close();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
await it("setting binaryType to 'arraybuffer' succeeds", async () => {
|
|
108
|
-
const pc = new RTCPeerConnection();
|
|
109
|
-
const dc = pc.createDataChannel('wpt');
|
|
110
|
-
dc.binaryType = 'arraybuffer';
|
|
111
|
-
expect(dc.binaryType).toBe('arraybuffer');
|
|
112
|
-
pc.close();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
await it("setting binaryType to 'blob' either succeeds or throws NotSupportedError when Blob is unavailable", async () => {
|
|
116
|
-
const pc = new RTCPeerConnection();
|
|
117
|
-
const dc = pc.createDataChannel('wpt');
|
|
118
|
-
let thrown: Error | null = null;
|
|
119
|
-
try { dc.binaryType = 'blob'; } catch (e: any) { thrown = e; }
|
|
120
|
-
if (typeof (globalThis as any).Blob === 'undefined') {
|
|
121
|
-
// Our documented deviation: no Blob → NotSupportedError.
|
|
122
|
-
expect(thrown).toBeDefined();
|
|
123
|
-
expect((thrown as any)?.name).toBe('NotSupportedError');
|
|
124
|
-
} else {
|
|
125
|
-
expect(thrown).toBeNull();
|
|
126
|
-
expect(dc.binaryType).toBe('blob');
|
|
127
|
-
}
|
|
128
|
-
pc.close();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Per WPT: invalid binaryType assignments are silently ignored
|
|
132
|
-
// (keep the previous value). Matches Firefox / Chrome / Safari.
|
|
133
|
-
const invalidBinaryTypes: any[] = ['jellyfish', 'arraybuffer ', '', null, undefined, 234];
|
|
134
|
-
for (const invalid of invalidBinaryTypes) {
|
|
135
|
-
await it(`setting binaryType to ${JSON.stringify(invalid)} is silently ignored`, async () => {
|
|
136
|
-
const pc = new RTCPeerConnection();
|
|
137
|
-
const dc = pc.createDataChannel('wpt');
|
|
138
|
-
dc.binaryType = 'arraybuffer';
|
|
139
|
-
dc.binaryType = invalid;
|
|
140
|
-
expect(dc.binaryType).toBe('arraybuffer');
|
|
141
|
-
pc.close();
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
await describe('WPT — RTCDataChannelInit-maxPacketLifeTime-enforce-range.html', async () => {
|
|
147
|
-
// Ported: refs/wpt/webrtc/RTCDataChannelInit-maxPacketLifeTime-enforce-range.html
|
|
148
|
-
//
|
|
149
|
-
// Web-IDL `[EnforceRange] unsigned short` — value must be 0-65535.
|
|
150
|
-
// Values outside range (or non-numeric) must throw TypeError.
|
|
151
|
-
// String values that parse to valid numbers are accepted.
|
|
152
|
-
|
|
153
|
-
if (!pipelineReady) {
|
|
154
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
155
|
-
expect(pipelineReady).toBeFalsy();
|
|
156
|
-
});
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const validValues = [0, 1, 3, 10, 1000, 65534, 65535];
|
|
161
|
-
for (const value of validValues) {
|
|
162
|
-
await it(`maxPacketLifeTime=${value} is accepted`, async () => {
|
|
163
|
-
const pc = new RTCPeerConnection();
|
|
164
|
-
const ch = pc.createDataChannel('t', { maxPacketLifeTime: value });
|
|
165
|
-
expect(ch.maxPacketLifeTime).toBe(value);
|
|
166
|
-
pc.close();
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const badValues: Array<[unknown, string]> = [
|
|
171
|
-
[-1, 'value -1'],
|
|
172
|
-
[-100, 'value -100'],
|
|
173
|
-
[65536, 'value 65536'],
|
|
174
|
-
[100000, 'value 100000'],
|
|
175
|
-
[Infinity, 'Infinity'],
|
|
176
|
-
[-Infinity, '-Infinity'],
|
|
177
|
-
[NaN, 'NaN'],
|
|
178
|
-
['65536', 'string "65536"'],
|
|
179
|
-
];
|
|
180
|
-
for (const [value, desc] of badValues) {
|
|
181
|
-
await it(`maxPacketLifeTime=${desc} throws TypeError`, async () => {
|
|
182
|
-
const pc = new RTCPeerConnection();
|
|
183
|
-
expect(() => pc.createDataChannel('t', { maxPacketLifeTime: value as number })).toThrow();
|
|
184
|
-
pc.close();
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
await it('maxPacketLifeTime="100" is coerced to 100', async () => {
|
|
189
|
-
const pc = new RTCPeerConnection();
|
|
190
|
-
const ch = pc.createDataChannel('t', { maxPacketLifeTime: '100' as unknown as number });
|
|
191
|
-
expect(ch.maxPacketLifeTime).toBe(100);
|
|
192
|
-
pc.close();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
await it('maxPacketLifeTime omitted returns null', async () => {
|
|
196
|
-
const pc = new RTCPeerConnection();
|
|
197
|
-
const ch = pc.createDataChannel('t', {});
|
|
198
|
-
expect(ch.maxPacketLifeTime).toBeNull();
|
|
199
|
-
pc.close();
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
await describe('WPT — RTCDataChannelInit-maxRetransmits-enforce-range.html', async () => {
|
|
204
|
-
// Ported: refs/wpt/webrtc/RTCDataChannelInit-maxRetransmits-enforce-range.html
|
|
205
|
-
//
|
|
206
|
-
// Same coercion rules as maxPacketLifeTime — sharing a single
|
|
207
|
-
// `coerceUnsignedShort` helper in the implementation.
|
|
208
|
-
|
|
209
|
-
if (!pipelineReady) {
|
|
210
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
211
|
-
expect(pipelineReady).toBeFalsy();
|
|
212
|
-
});
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const validValues = [0, 1, 3, 10, 1000, 65534, 65535];
|
|
217
|
-
for (const value of validValues) {
|
|
218
|
-
await it(`maxRetransmits=${value} is accepted`, async () => {
|
|
219
|
-
const pc = new RTCPeerConnection();
|
|
220
|
-
const ch = pc.createDataChannel('t', { maxRetransmits: value });
|
|
221
|
-
expect(ch.maxRetransmits).toBe(value);
|
|
222
|
-
pc.close();
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const badValues: Array<[unknown, string]> = [
|
|
227
|
-
[-1, 'value -1'],
|
|
228
|
-
[65536, 'value 65536'],
|
|
229
|
-
[Infinity, 'Infinity'],
|
|
230
|
-
[NaN, 'NaN'],
|
|
231
|
-
['65536', 'string "65536"'],
|
|
232
|
-
];
|
|
233
|
-
for (const [value, desc] of badValues) {
|
|
234
|
-
await it(`maxRetransmits=${desc} throws TypeError`, async () => {
|
|
235
|
-
const pc = new RTCPeerConnection();
|
|
236
|
-
expect(() => pc.createDataChannel('t', { maxRetransmits: value as number })).toThrow();
|
|
237
|
-
pc.close();
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
await it('maxRetransmits="100" is coerced to 100', async () => {
|
|
242
|
-
const pc = new RTCPeerConnection();
|
|
243
|
-
const ch = pc.createDataChannel('t', { maxRetransmits: '100' as unknown as number });
|
|
244
|
-
expect(ch.maxRetransmits).toBe(100);
|
|
245
|
-
pc.close();
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
await it('maxRetransmits omitted returns null', async () => {
|
|
249
|
-
const pc = new RTCPeerConnection();
|
|
250
|
-
const ch = pc.createDataChannel('t', {});
|
|
251
|
-
expect(ch.maxRetransmits).toBeNull();
|
|
252
|
-
pc.close();
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
await describe('WPT — RTCPeerConnection-createDataChannel.html', async () => {
|
|
257
|
-
// Ported: refs/wpt/webrtc/RTCPeerConnection-createDataChannel.html
|
|
258
|
-
//
|
|
259
|
-
// Covers the validation rules for RTCPeerConnection.createDataChannel(label, opts).
|
|
260
|
-
|
|
261
|
-
if (!pipelineReady) {
|
|
262
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
263
|
-
expect(pipelineReady).toBeFalsy();
|
|
264
|
-
});
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
await it('creates with default label and init defaults', async () => {
|
|
269
|
-
const pc = new RTCPeerConnection();
|
|
270
|
-
const dc = pc.createDataChannel('');
|
|
271
|
-
expect(dc instanceof RTCDataChannel).toBeTruthy();
|
|
272
|
-
expect(dc.label).toBe('');
|
|
273
|
-
expect(dc.ordered).toBeTruthy();
|
|
274
|
-
expect(dc.maxPacketLifeTime).toBeNull();
|
|
275
|
-
expect(dc.maxRetransmits).toBeNull();
|
|
276
|
-
expect(dc.protocol).toBe('');
|
|
277
|
-
expect(dc.negotiated).toBeFalsy();
|
|
278
|
-
pc.close();
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
await it('throws when both maxPacketLifeTime and maxRetransmits are set', async () => {
|
|
282
|
-
const pc = new RTCPeerConnection();
|
|
283
|
-
expect(() => pc.createDataChannel('wpt', {
|
|
284
|
-
maxPacketLifeTime: 1000,
|
|
285
|
-
maxRetransmits: 5,
|
|
286
|
-
})).toThrow();
|
|
287
|
-
pc.close();
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
await it('throws when negotiated=true without id', async () => {
|
|
291
|
-
const pc = new RTCPeerConnection();
|
|
292
|
-
expect(() => pc.createDataChannel('wpt', { negotiated: true })).toThrow();
|
|
293
|
-
pc.close();
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
await it('throws when id is out of range', async () => {
|
|
297
|
-
const pc = new RTCPeerConnection();
|
|
298
|
-
expect(() => pc.createDataChannel('wpt', { id: 65535 })).toThrow();
|
|
299
|
-
expect(() => pc.createDataChannel('wpt', { id: -1 })).toThrow();
|
|
300
|
-
pc.close();
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
await it('throws InvalidStateError after close()', async () => {
|
|
304
|
-
const pc = new RTCPeerConnection();
|
|
305
|
-
pc.close();
|
|
306
|
-
let thrown: any = null;
|
|
307
|
-
try { pc.createDataChannel('wpt'); } catch (e) { thrown = e; }
|
|
308
|
-
expect(thrown).toBeDefined();
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
await describe('WPT — RTCConfiguration-iceServers.html', async () => {
|
|
313
|
-
// Ported: refs/wpt/webrtc/RTCConfiguration-iceServers.html
|
|
314
|
-
//
|
|
315
|
-
// ICE-server URL parsing + credential validation. The browsers throw
|
|
316
|
-
// SyntaxError (for invalid URLs) or TypeError (for missing TURN creds).
|
|
317
|
-
|
|
318
|
-
if (!pipelineReady) {
|
|
319
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
320
|
-
expect(pipelineReady).toBeFalsy();
|
|
321
|
-
});
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
await it('accepts a single STUN URL', async () => {
|
|
326
|
-
const pc = new RTCPeerConnection({
|
|
327
|
-
iceServers: [{ urls: 'stun:stun.example.com:19302' }],
|
|
328
|
-
});
|
|
329
|
-
expect(pc).toBeDefined();
|
|
330
|
-
pc.close();
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
await it('accepts an array of STUN URLs', async () => {
|
|
334
|
-
const pc = new RTCPeerConnection({
|
|
335
|
-
iceServers: [{ urls: ['stun:stun1.example.com', 'stun:stun2.example.com'] }],
|
|
336
|
-
});
|
|
337
|
-
expect(pc).toBeDefined();
|
|
338
|
-
pc.close();
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
await it('throws SyntaxError for empty urls array', async () => {
|
|
342
|
-
expect(() =>
|
|
343
|
-
new RTCPeerConnection({ iceServers: [{ urls: [] }] })
|
|
344
|
-
).toThrow();
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
await it('throws TypeError for TURN without credentials', async () => {
|
|
348
|
-
expect(() => new RTCPeerConnection({
|
|
349
|
-
iceServers: [{ urls: 'turn:turn.example.com' }],
|
|
350
|
-
} as any)).toThrow();
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
await it('accepts TURN with username + credential', async () => {
|
|
354
|
-
const pc = new RTCPeerConnection({
|
|
355
|
-
iceServers: [{
|
|
356
|
-
urls: 'turn:turn.example.com:3478',
|
|
357
|
-
username: 'alice',
|
|
358
|
-
credential: 'secret',
|
|
359
|
-
}],
|
|
360
|
-
});
|
|
361
|
-
expect(pc).toBeDefined();
|
|
362
|
-
pc.close();
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
await it('throws TypeError for unknown scheme', async () => {
|
|
366
|
-
expect(() => new RTCPeerConnection({
|
|
367
|
-
iceServers: [{ urls: 'wss://example.com' }],
|
|
368
|
-
})).toThrow();
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
await describe('WPT — RTCPeerConnection-createOffer.html', async () => {
|
|
373
|
-
// Ported: refs/wpt/webrtc/RTCPeerConnection-createOffer.html
|
|
374
|
-
//
|
|
375
|
-
// Tests the base createOffer flow + error cases. We skip the
|
|
376
|
-
// `generateVideoReceiveOnlyOffer` / `addTransceiver('audio')`
|
|
377
|
-
// tests because those require media support (Phase 2).
|
|
378
|
-
|
|
379
|
-
if (!pipelineReady) {
|
|
380
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
381
|
-
expect(pipelineReady).toBeFalsy();
|
|
382
|
-
});
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
await it("createOffer() returns a plain object, not an RTCSessionDescription instance", async () => {
|
|
387
|
-
const pc = new RTCPeerConnection();
|
|
388
|
-
try {
|
|
389
|
-
const offer = await withTimeout(5000, pc.createOffer() as Promise<any>, 'createOffer');
|
|
390
|
-
expect(typeof offer).toBe('object');
|
|
391
|
-
// Per spec: createOffer returns RTCSessionDescriptionInit
|
|
392
|
-
// (a dictionary), not an RTCSessionDescription instance.
|
|
393
|
-
expect(offer instanceof RTCSessionDescription).toBeFalsy();
|
|
394
|
-
} finally {
|
|
395
|
-
pc.close();
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
await it("createOffer() returns { type: 'offer', sdp: string }", async () => {
|
|
400
|
-
const pc = new RTCPeerConnection();
|
|
401
|
-
try {
|
|
402
|
-
const offer = await withTimeout(5000, pc.createOffer() as Promise<any>, 'createOffer');
|
|
403
|
-
expect(offer.type).toBe('offer');
|
|
404
|
-
expect(typeof offer.sdp).toBe('string');
|
|
405
|
-
} finally {
|
|
406
|
-
pc.close();
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
await it("createOffer() with a data channel produces m=application line", async () => {
|
|
411
|
-
const pc = new RTCPeerConnection();
|
|
412
|
-
try {
|
|
413
|
-
pc.createDataChannel('wpt');
|
|
414
|
-
const offer = await withTimeout(5000, pc.createOffer() as Promise<any>, 'createOffer+dc');
|
|
415
|
-
expect(offer.type).toBe('offer');
|
|
416
|
-
expect(offer.sdp).toContain('m=application');
|
|
417
|
-
expect(offer.sdp).toContain('webrtc-datachannel');
|
|
418
|
-
} finally {
|
|
419
|
-
pc.close();
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
await it("createOffer() after close() rejects with InvalidStateError", async () => {
|
|
424
|
-
const pc = new RTCPeerConnection();
|
|
425
|
-
pc.close();
|
|
426
|
-
let thrown: any = null;
|
|
427
|
-
try { await pc.createOffer(); } catch (e) { thrown = e; }
|
|
428
|
-
expect(thrown).toBeDefined();
|
|
429
|
-
expect((thrown as any)?.name).toBe('InvalidStateError');
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
await it("createOffer() + setLocalDescription() transitions to 'have-local-offer'", async () => {
|
|
433
|
-
const pc = new RTCPeerConnection();
|
|
434
|
-
try {
|
|
435
|
-
pc.createDataChannel('wpt'); // give the offer content
|
|
436
|
-
const states: string[] = [];
|
|
437
|
-
pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
|
|
438
|
-
const offer = await withTimeout(5000, pc.createOffer() as Promise<any>, 'createOffer');
|
|
439
|
-
await withTimeout(5000, pc.setLocalDescription(offer), 'setLocalDescription');
|
|
440
|
-
expect(pc.signalingState).toBe('have-local-offer');
|
|
441
|
-
expect(pc.localDescription).toBeDefined();
|
|
442
|
-
expect(pc.localDescription!.type).toBe('offer');
|
|
443
|
-
// currentLocalDescription is null until the answer arrives.
|
|
444
|
-
expect(pc.currentLocalDescription).toBeNull();
|
|
445
|
-
expect(states).toContain('have-local-offer');
|
|
446
|
-
} finally {
|
|
447
|
-
pc.close();
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
await describe('WPT — RTCPeerConnection close / InvalidStateError after close', async () => {
|
|
453
|
-
// Ported: refs/wpt/webrtc/RTCPeerConnection-close* variants.
|
|
454
|
-
// After close(), all async methods must reject with InvalidStateError.
|
|
455
|
-
|
|
456
|
-
if (!pipelineReady) {
|
|
457
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
458
|
-
expect(pipelineReady).toBeFalsy();
|
|
459
|
-
});
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const asyncMethods: Array<[string, (pc: RTCPeerConnection) => Promise<unknown>]> = [
|
|
464
|
-
['createOffer', (pc) => pc.createOffer() as Promise<unknown>],
|
|
465
|
-
['createAnswer', (pc) => pc.createAnswer() as Promise<unknown>],
|
|
466
|
-
['setLocalDescription', (pc) => pc.setLocalDescription({ type: 'offer', sdp: 'v=0\r\n' })],
|
|
467
|
-
['setRemoteDescription', (pc) => pc.setRemoteDescription({ type: 'offer', sdp: 'v=0\r\n' })],
|
|
468
|
-
['addIceCandidate', (pc) => pc.addIceCandidate({ candidate: '', sdpMLineIndex: 0 })],
|
|
469
|
-
];
|
|
470
|
-
|
|
471
|
-
for (const [name, fn] of asyncMethods) {
|
|
472
|
-
await it(`${name}() after close() rejects with InvalidStateError`, async () => {
|
|
473
|
-
const pc = new RTCPeerConnection();
|
|
474
|
-
pc.close();
|
|
475
|
-
let thrown: any = null;
|
|
476
|
-
try { await fn(pc); } catch (e) { thrown = e; }
|
|
477
|
-
expect(thrown).toBeDefined();
|
|
478
|
-
expect((thrown as any)?.name).toBe('InvalidStateError');
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
await describe('WPT — getConfiguration round-trip', async () => {
|
|
484
|
-
// Ported subset from: refs/wpt/webrtc/RTCPeerConnection-getConfiguration.html
|
|
485
|
-
//
|
|
486
|
-
// getConfiguration() returns a copy of the configuration — mutating
|
|
487
|
-
// the returned object must not affect the peer connection's state.
|
|
488
|
-
|
|
489
|
-
if (!pipelineReady) {
|
|
490
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
491
|
-
expect(pipelineReady).toBeFalsy();
|
|
492
|
-
});
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
await it("getConfiguration() returns defaults when none were passed", async () => {
|
|
497
|
-
const pc = new RTCPeerConnection();
|
|
498
|
-
const cfg = pc.getConfiguration();
|
|
499
|
-
// Default iceServers: not set (undefined) — spec allows either.
|
|
500
|
-
// We just check that we get back an object.
|
|
501
|
-
expect(typeof cfg).toBe('object');
|
|
502
|
-
pc.close();
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
await it("getConfiguration() preserves iceServers", async () => {
|
|
506
|
-
const pc = new RTCPeerConnection({
|
|
507
|
-
iceServers: [{ urls: 'stun:stun.example.com:19302' }],
|
|
508
|
-
});
|
|
509
|
-
const cfg = pc.getConfiguration();
|
|
510
|
-
expect(Array.isArray(cfg.iceServers)).toBeTruthy();
|
|
511
|
-
expect(cfg.iceServers!.length).toBe(1);
|
|
512
|
-
pc.close();
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
await it("getConfiguration() returns a copy — mutating it doesn't alter the PC", async () => {
|
|
516
|
-
const pc = new RTCPeerConnection({
|
|
517
|
-
iceServers: [{ urls: 'stun:stun.example.com:19302' }],
|
|
518
|
-
});
|
|
519
|
-
const cfg1 = pc.getConfiguration();
|
|
520
|
-
(cfg1 as any).iceServers = [];
|
|
521
|
-
const cfg2 = pc.getConfiguration();
|
|
522
|
-
expect(cfg2.iceServers!.length).toBe(1);
|
|
523
|
-
pc.close();
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
await describe('WPT — RTCDataChannel-id.html (subset)', async () => {
|
|
528
|
-
// Ported subset of: refs/wpt/webrtc/RTCDataChannel-id.html
|
|
529
|
-
//
|
|
530
|
-
// Spec §6.1.1.3: for non-negotiated channels, `id` is null until
|
|
531
|
-
// the SCTP transport connects (then assigned based on DTLS role).
|
|
532
|
-
// For negotiated=true channels, the user-provided id is kept.
|
|
533
|
-
//
|
|
534
|
-
// We skip the DTLS-role-based odd/even-id tests because they
|
|
535
|
-
// require `pc.sctp` which we don't implement (Phase 3).
|
|
536
|
-
|
|
537
|
-
if (!pipelineReady) {
|
|
538
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
539
|
-
expect(pipelineReady).toBeFalsy();
|
|
540
|
-
});
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
await it("id is null for non-negotiated channel before SCTP connects", async () => {
|
|
545
|
-
const pc = new RTCPeerConnection();
|
|
546
|
-
try {
|
|
547
|
-
const dc = pc.createDataChannel('wpt');
|
|
548
|
-
expect(dc.id).toBeNull();
|
|
549
|
-
} finally {
|
|
550
|
-
pc.close();
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
await it("negotiated=true + id=42 preserves the user-provided id", async () => {
|
|
555
|
-
const pc = new RTCPeerConnection();
|
|
556
|
-
try {
|
|
557
|
-
const dc = pc.createDataChannel('wpt', { negotiated: true, id: 42 });
|
|
558
|
-
expect(dc.id).toBe(42);
|
|
559
|
-
} finally {
|
|
560
|
-
pc.close();
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
await it("negotiated=true + id=0 is allowed (0 is a valid id)", async () => {
|
|
565
|
-
const pc = new RTCPeerConnection();
|
|
566
|
-
try {
|
|
567
|
-
const dc = pc.createDataChannel('wpt', { negotiated: true, id: 0 });
|
|
568
|
-
expect(dc.id).toBe(0);
|
|
569
|
-
} finally {
|
|
570
|
-
pc.close();
|
|
571
|
-
}
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
await it("id=65535 throws TypeError (reserved per RFC 8832)", async () => {
|
|
575
|
-
const pc = new RTCPeerConnection();
|
|
576
|
-
try {
|
|
577
|
-
expect(() => pc.createDataChannel('wpt', { negotiated: true, id: 65535 })).toThrow();
|
|
578
|
-
} finally {
|
|
579
|
-
pc.close();
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
await describe('WPT — signalingstatechange sequencing', async () => {
|
|
585
|
-
// Ported subset of: refs/wpt/webrtc/RTCPeerConnection-onsignalingstatechanged.https.html
|
|
586
|
-
//
|
|
587
|
-
// Negotiation methods must fire signalingstatechange events in the
|
|
588
|
-
// correct order. We skip the tests that require media tracks; the
|
|
589
|
-
// data-channel variant still exercises the same state machine.
|
|
590
|
-
|
|
591
|
-
if (!pipelineReady) {
|
|
592
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
593
|
-
expect(pipelineReady).toBeFalsy();
|
|
594
|
-
});
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
await it("setRemoteDescription(offer) transitions B to 'have-remote-offer'", async () => {
|
|
599
|
-
const pcA = new RTCPeerConnection();
|
|
600
|
-
const pcB = new RTCPeerConnection();
|
|
601
|
-
try {
|
|
602
|
-
pcA.createDataChannel('wpt');
|
|
603
|
-
const events: string[] = [];
|
|
604
|
-
pcB.addEventListener('signalingstatechange', () => events.push(pcB.signalingState));
|
|
605
|
-
|
|
606
|
-
const offer = await withTimeout(5000, pcA.createOffer() as Promise<any>, 'createOffer');
|
|
607
|
-
await withTimeout(5000, pcA.setLocalDescription(offer), 'setLocalDescription');
|
|
608
|
-
await withTimeout(5000, pcB.setRemoteDescription(offer), 'setRemoteDescription');
|
|
609
|
-
|
|
610
|
-
expect(events.length).toBeGreaterThan(0);
|
|
611
|
-
expect(events[0]).toBe('have-remote-offer');
|
|
612
|
-
expect(pcB.signalingState).toBe('have-remote-offer');
|
|
613
|
-
} finally {
|
|
614
|
-
pcA.close();
|
|
615
|
-
pcB.close();
|
|
616
|
-
}
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
await it("close() does not fire additional signalingstatechange events", async () => {
|
|
620
|
-
const pc = new RTCPeerConnection();
|
|
621
|
-
let eventsAfterClose = 0;
|
|
622
|
-
pc.close();
|
|
623
|
-
pc.addEventListener('signalingstatechange', () => { eventsAfterClose++; });
|
|
624
|
-
// Wait a tick for any queued events to drain.
|
|
625
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
626
|
-
expect(pc.signalingState).toBe('closed');
|
|
627
|
-
expect(eventsAfterClose).toBe(0);
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
await describe('WPT — RTCConfiguration-validation.html (subset)', async () => {
|
|
632
|
-
// Ported: refs/wpt/webrtc/RTCConfiguration-validation.html + the
|
|
633
|
-
// bundlePolicy / iceTransportPolicy validation tests.
|
|
634
|
-
//
|
|
635
|
-
// Atomic setConfiguration (invalid input must leave state unchanged)
|
|
636
|
-
// is tracked in Phase 3 — we throw NotSupportedError right now.
|
|
637
|
-
|
|
638
|
-
if (!pipelineReady) {
|
|
639
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
640
|
-
expect(pipelineReady).toBeFalsy();
|
|
641
|
-
});
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
await it("accepts iceTransportPolicy='all' (default)", async () => {
|
|
646
|
-
const pc = new RTCPeerConnection({ iceTransportPolicy: 'all' });
|
|
647
|
-
expect(pc).toBeDefined();
|
|
648
|
-
pc.close();
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
await it("accepts iceTransportPolicy='relay'", async () => {
|
|
652
|
-
const pc = new RTCPeerConnection({ iceTransportPolicy: 'relay' });
|
|
653
|
-
expect(pc).toBeDefined();
|
|
654
|
-
pc.close();
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
await it("accepts bundlePolicy='balanced' / 'max-compat' / 'max-bundle'", async () => {
|
|
658
|
-
for (const policy of ['balanced', 'max-compat', 'max-bundle'] as const) {
|
|
659
|
-
const pc = new RTCPeerConnection({ bundlePolicy: policy });
|
|
660
|
-
expect(pc).toBeDefined();
|
|
661
|
-
pc.close();
|
|
662
|
-
}
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
await it("setConfiguration throws NotSupportedError (not yet implemented)", async () => {
|
|
666
|
-
const pc = new RTCPeerConnection();
|
|
667
|
-
let thrown: any = null;
|
|
668
|
-
try { (pc as any).setConfiguration({ iceTransportPolicy: 'all' }); } catch (e) { thrown = e; }
|
|
669
|
-
expect(thrown).toBeDefined();
|
|
670
|
-
expect((thrown as any)?.name).toBe('NotSupportedError');
|
|
671
|
-
pc.close();
|
|
672
|
-
});
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
await describe('WPT — RTCDataChannel-bufferedAmount.html (subset)', async () => {
|
|
676
|
-
// Ported subset of: refs/wpt/webrtc/RTCDataChannel-bufferedAmount.html
|
|
677
|
-
//
|
|
678
|
-
// Spec §6.2: bufferedAmount increases by:
|
|
679
|
-
// - UTF-8 byte length for strings
|
|
680
|
-
// - byte length for ArrayBuffer / ArrayBufferView
|
|
681
|
-
// - size for Blob
|
|
682
|
-
// The "unicodeString should increase bufferedAmount by UTF-8 byte
|
|
683
|
-
// length" test is the interesting one — naive implementations
|
|
684
|
-
// increment by .length (UTF-16 code units) instead of UTF-8 bytes.
|
|
685
|
-
|
|
686
|
-
if (!pipelineReady) {
|
|
687
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
688
|
-
expect(pipelineReady).toBeFalsy();
|
|
689
|
-
});
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const helloBuffer = Uint8Array.of(0x68, 0x65, 0x6c, 0x6c, 0x6f);
|
|
694
|
-
const unicodeString = '世界你好';
|
|
695
|
-
const unicodeBytes = 12;
|
|
696
|
-
|
|
697
|
-
await it('bufferedAmount starts at 0 for both peers', async () => {
|
|
698
|
-
const [dc1, dc2, pc1, pc2] = await withTimeout(
|
|
699
|
-
15000,
|
|
700
|
-
createDataChannelPair({}),
|
|
701
|
-
'createDataChannelPair',
|
|
702
|
-
);
|
|
703
|
-
try {
|
|
704
|
-
expect(dc1.bufferedAmount).toBe(0);
|
|
705
|
-
expect(dc2.bufferedAmount).toBe(0);
|
|
706
|
-
} finally {
|
|
707
|
-
closePeerConnections(pc1, pc2);
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
await it('bufferedAmount increases by UTF-8 byte length when sending a unicode string (not UTF-16 length)', async () => {
|
|
712
|
-
const [dc1, dc2, pc1, pc2] = await withTimeout(
|
|
713
|
-
15000,
|
|
714
|
-
createDataChannelPair({}),
|
|
715
|
-
'createDataChannelPair',
|
|
716
|
-
);
|
|
717
|
-
try {
|
|
718
|
-
dc1.send(unicodeString);
|
|
719
|
-
// The CJK string has 4 UTF-16 code units but 12 UTF-8 bytes.
|
|
720
|
-
expect(dc1.bufferedAmount).toBe(unicodeBytes);
|
|
721
|
-
expect(dc1.bufferedAmount).not.toBe(unicodeString.length);
|
|
722
|
-
await withTimeout(5000, awaitMessage(dc2), 'awaitMessage');
|
|
723
|
-
} finally {
|
|
724
|
-
closePeerConnections(pc1, pc2);
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
await it('bufferedAmount increases by byte length when sending an ArrayBuffer', async () => {
|
|
729
|
-
const [dc1, dc2, pc1, pc2] = await withTimeout(
|
|
730
|
-
15000,
|
|
731
|
-
createDataChannelPair({}),
|
|
732
|
-
'createDataChannelPair',
|
|
733
|
-
);
|
|
734
|
-
try {
|
|
735
|
-
dc1.send(helloBuffer.buffer);
|
|
736
|
-
expect(dc1.bufferedAmount).toBe(helloBuffer.byteLength);
|
|
737
|
-
await withTimeout(5000, awaitMessage(dc2), 'awaitMessage');
|
|
738
|
-
} finally {
|
|
739
|
-
closePeerConnections(pc1, pc2);
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
await it('bufferedAmount stays at 0 for empty string', async () => {
|
|
744
|
-
const [dc1, dc2, pc1, pc2] = await withTimeout(
|
|
745
|
-
15000,
|
|
746
|
-
createDataChannelPair({}),
|
|
747
|
-
'createDataChannelPair',
|
|
748
|
-
);
|
|
749
|
-
try {
|
|
750
|
-
dc1.send('');
|
|
751
|
-
expect(dc1.bufferedAmount).toBe(0);
|
|
752
|
-
} finally {
|
|
753
|
-
closePeerConnections(pc1, pc2);
|
|
754
|
-
}
|
|
755
|
-
});
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
await describe('WPT — RTCDataChannel-close.html (subset)', async () => {
|
|
759
|
-
// Ported subset of: refs/wpt/webrtc/RTCDataChannel-close.html
|
|
760
|
-
//
|
|
761
|
-
// close() on one peer should propagate to the other; both transition
|
|
762
|
-
// readyState to 'closed' and fire the 'close' event.
|
|
763
|
-
|
|
764
|
-
if (!pipelineReady) {
|
|
765
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
766
|
-
expect(pipelineReady).toBeFalsy();
|
|
767
|
-
});
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
await it("close() transitions readyState to 'closed' on the caller", async () => {
|
|
772
|
-
const [dc1, _dc2, pc1, pc2] = await withTimeout(
|
|
773
|
-
15000,
|
|
774
|
-
createDataChannelPair({}),
|
|
775
|
-
'createDataChannelPair',
|
|
776
|
-
);
|
|
777
|
-
try {
|
|
778
|
-
dc1.close();
|
|
779
|
-
expect(dc1.readyState).toBe('closed');
|
|
780
|
-
} finally {
|
|
781
|
-
closePeerConnections(pc1, pc2);
|
|
782
|
-
}
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
await it("close() on one peer fires 'close' on the other (close propagates)", async () => {
|
|
786
|
-
const [dc1, dc2, pc1, pc2] = await withTimeout(
|
|
787
|
-
15000,
|
|
788
|
-
createDataChannelPair({}),
|
|
789
|
-
'createDataChannelPair',
|
|
790
|
-
);
|
|
791
|
-
try {
|
|
792
|
-
const closedOnB = new Promise<void>((resolve) => {
|
|
793
|
-
if (dc2.readyState === 'closed') return resolve();
|
|
794
|
-
dc2.addEventListener('close', () => resolve(), { once: true });
|
|
795
|
-
});
|
|
796
|
-
dc1.close();
|
|
797
|
-
await withTimeout(5000, closedOnB, 'dc2 close propagation');
|
|
798
|
-
expect(dc2.readyState).toBe('closed');
|
|
799
|
-
} finally {
|
|
800
|
-
closePeerConnections(pc1, pc2);
|
|
801
|
-
}
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
await it('send() after close() throws InvalidStateError', async () => {
|
|
805
|
-
const pc = new RTCPeerConnection();
|
|
806
|
-
const dc = pc.createDataChannel('wpt');
|
|
807
|
-
dc.close();
|
|
808
|
-
let thrown: any = null;
|
|
809
|
-
try { dc.send('hi'); } catch (e) { thrown = e; }
|
|
810
|
-
expect(thrown).toBeDefined();
|
|
811
|
-
expect((thrown as any)?.name).toBe('InvalidStateError');
|
|
812
|
-
pc.close();
|
|
813
|
-
});
|
|
814
|
-
});
|
|
815
|
-
|
|
816
|
-
await describe('WPT — RTCIceCandidate constructor', async () => {
|
|
817
|
-
// Ported from refs/wpt/webrtc/RTCIceCandidate.html — the throwing /
|
|
818
|
-
// accepting / parsing rules.
|
|
819
|
-
|
|
820
|
-
await it('constructs with candidate + sdpMLineIndex', async () => {
|
|
821
|
-
const c = new RTCIceCandidate({ candidate: '', sdpMLineIndex: 0 });
|
|
822
|
-
expect(c.candidate).toBe('');
|
|
823
|
-
expect(c.sdpMLineIndex).toBe(0);
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
await it('constructs with candidate + sdpMid', async () => {
|
|
827
|
-
const c = new RTCIceCandidate({ candidate: '', sdpMid: '0' });
|
|
828
|
-
expect(c.sdpMid).toBe('0');
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
await it('throws TypeError without sdpMid AND sdpMLineIndex', async () => {
|
|
832
|
-
expect(() => new RTCIceCandidate({ candidate: 'candidate:…' })).toThrow();
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
await it('toJSON round-trip preserves fields', async () => {
|
|
836
|
-
const c = new RTCIceCandidate({
|
|
837
|
-
candidate: 'candidate:1 1 udp 100 1.2.3.4 1234 typ host',
|
|
838
|
-
sdpMid: '0',
|
|
839
|
-
sdpMLineIndex: 0,
|
|
840
|
-
usernameFragment: 'abcd',
|
|
841
|
-
});
|
|
842
|
-
const j = c.toJSON();
|
|
843
|
-
const c2 = new RTCIceCandidate(j);
|
|
844
|
-
expect(c2.candidate).toBe(c.candidate);
|
|
845
|
-
expect(c2.sdpMid).toBe(c.sdpMid);
|
|
846
|
-
expect(c2.sdpMLineIndex).toBe(c.sdpMLineIndex);
|
|
847
|
-
expect(c2.usernameFragment).toBe(c.usernameFragment);
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
await it('parses the candidate-line into structured fields', async () => {
|
|
851
|
-
const c = new RTCIceCandidate({
|
|
852
|
-
candidate: 'candidate:842163049 1 udp 1677729535 1.2.3.4 12345 typ srflx raddr 10.0.0.1 rport 5000',
|
|
853
|
-
sdpMid: '0',
|
|
854
|
-
});
|
|
855
|
-
expect(c.protocol).toBe('udp');
|
|
856
|
-
expect(c.address).toBe('1.2.3.4');
|
|
857
|
-
expect(c.port).toBe(12345);
|
|
858
|
-
expect(c.type).toBe('srflx');
|
|
859
|
-
expect(c.component).toBe('rtp');
|
|
860
|
-
expect(c.relatedAddress).toBe('10.0.0.1');
|
|
861
|
-
expect(c.relatedPort).toBe(5000);
|
|
862
|
-
});
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
await describe('WPT — RTCError / RTCErrorEvent', async () => {
|
|
866
|
-
// Ported from refs/wpt/webrtc/RTCError.html + RTCErrorEvent-constructor.html
|
|
867
|
-
|
|
868
|
-
await it('RTCError extends DOMException', async () => {
|
|
869
|
-
const err = new RTCError({ errorDetail: 'data-channel-failure' }, 'msg');
|
|
870
|
-
expect(err instanceof DOMException).toBeTruthy();
|
|
871
|
-
expect(err.errorDetail).toBe('data-channel-failure');
|
|
872
|
-
expect(err.message).toBe('msg');
|
|
873
|
-
expect(err.name).toBe('OperationError');
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
await it('RTCError throws TypeError without errorDetail', async () => {
|
|
877
|
-
expect(() => new (RTCError as any)({})).toThrow();
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
await it('RTCErrorEvent carries its error instance', async () => {
|
|
881
|
-
const err = new RTCError({ errorDetail: 'dtls-failure' }, 'handshake failed');
|
|
882
|
-
const ev = new RTCErrorEvent('error', { error: err });
|
|
883
|
-
expect(ev.error).toBe(err);
|
|
884
|
-
expect(ev instanceof Event).toBeTruthy();
|
|
885
|
-
});
|
|
886
|
-
|
|
887
|
-
await it('RTCErrorEvent throws TypeError without error', async () => {
|
|
888
|
-
expect(() => new (RTCErrorEvent as any)('error', {})).toThrow();
|
|
889
|
-
});
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
await describe('WPT — RTCPeerConnectionIceEvent', async () => {
|
|
893
|
-
// Ported from refs/wpt/webrtc/RTCPeerConnectionIceEvent-constructor.html
|
|
894
|
-
|
|
895
|
-
await it('accepts { candidate: null }', async () => {
|
|
896
|
-
const ev = new RTCPeerConnectionIceEvent('icecandidate', { candidate: null });
|
|
897
|
-
expect(ev.candidate).toBeNull();
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
await it('accepts a real RTCIceCandidate', async () => {
|
|
901
|
-
const c = new RTCIceCandidate({ candidate: '', sdpMid: '0' });
|
|
902
|
-
const ev = new RTCPeerConnectionIceEvent('icecandidate', { candidate: c });
|
|
903
|
-
expect(ev.candidate).toBe(c);
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
await it('defaults url to null', async () => {
|
|
907
|
-
const ev = new RTCPeerConnectionIceEvent('icecandidate');
|
|
908
|
-
expect(ev.url).toBeNull();
|
|
909
|
-
});
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
await describe('WPT — RTCSessionDescription round-trip', async () => {
|
|
913
|
-
// Derived from various RTCSessionDescription-related WPT tests.
|
|
914
|
-
|
|
915
|
-
await it('serializes to JSON compatible with the init dict', async () => {
|
|
916
|
-
const sdp = 'v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\n';
|
|
917
|
-
const d = new RTCSessionDescription({ type: 'offer', sdp });
|
|
918
|
-
const j = d.toJSON();
|
|
919
|
-
expect(j.type).toBe('offer');
|
|
920
|
-
expect(j.sdp).toBe(sdp);
|
|
921
|
-
const d2 = new RTCSessionDescription(j);
|
|
922
|
-
expect(d2.type).toBe(d.type);
|
|
923
|
-
expect(d2.sdp).toBe(d.sdp);
|
|
924
|
-
});
|
|
925
|
-
});
|
|
926
|
-
|
|
927
|
-
// ── RTCDataChannel-send (data type fidelity) ─────────────────────────
|
|
928
|
-
// Ported from refs/wpt/webrtc/RTCDataChannel-send.html
|
|
929
|
-
// Tests: string, unicode, ArrayBuffer, Uint8Array round-trips over a
|
|
930
|
-
// connected data channel pair.
|
|
931
|
-
|
|
932
|
-
await describe('WPT — RTCDataChannel-send', async () => {
|
|
933
|
-
if (!pipelineReady) {
|
|
934
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
935
|
-
expect(pipelineReady).toBeFalsy();
|
|
936
|
-
});
|
|
937
|
-
} else {
|
|
938
|
-
await it('send/receive ASCII string', async () => {
|
|
939
|
-
const [chA, chB, pcA, pcB] = await createDataChannelPair();
|
|
940
|
-
try {
|
|
941
|
-
chA.send('hello world');
|
|
942
|
-
const msg = await withTimeout(5000, awaitMessage<string>(chB), 'ASCII string');
|
|
943
|
-
expect(msg).toBe('hello world');
|
|
944
|
-
} finally { closePeerConnections(pcA, pcB); }
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
await it('send/receive Unicode string', async () => {
|
|
948
|
-
const [chA, chB, pcA, pcB] = await createDataChannelPair();
|
|
949
|
-
try {
|
|
950
|
-
const unicode = '\u00fc\u00e4\u00f6\u2603\u{1F600}';
|
|
951
|
-
chA.send(unicode);
|
|
952
|
-
const msg = await withTimeout(5000, awaitMessage<string>(chB), 'Unicode string');
|
|
953
|
-
expect(msg).toBe(unicode);
|
|
954
|
-
} finally { closePeerConnections(pcA, pcB); }
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
await it('send/receive empty string', async () => {
|
|
958
|
-
const [chA, chB, pcA, pcB] = await createDataChannelPair();
|
|
959
|
-
try {
|
|
960
|
-
chA.send('');
|
|
961
|
-
const msg = await withTimeout(5000, awaitMessage<string>(chB), 'empty string');
|
|
962
|
-
expect(msg).toBe('');
|
|
963
|
-
} finally { closePeerConnections(pcA, pcB); }
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
await it('send/receive ArrayBuffer', async () => {
|
|
967
|
-
const [chA, chB, pcA, pcB] = await createDataChannelPair();
|
|
968
|
-
try {
|
|
969
|
-
chB.binaryType = 'arraybuffer';
|
|
970
|
-
const data = new Uint8Array([1, 2, 3, 4, 5]).buffer;
|
|
971
|
-
chA.send(data);
|
|
972
|
-
const msg = await withTimeout(5000, awaitMessage<ArrayBuffer>(chB), 'ArrayBuffer');
|
|
973
|
-
const arr = new Uint8Array(msg);
|
|
974
|
-
expect(arr.length).toBe(5);
|
|
975
|
-
expect(arr[0]).toBe(1);
|
|
976
|
-
expect(arr[4]).toBe(5);
|
|
977
|
-
} finally { closePeerConnections(pcA, pcB); }
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
await it('send/receive Uint8Array view', async () => {
|
|
981
|
-
const [chA, chB, pcA, pcB] = await createDataChannelPair();
|
|
982
|
-
try {
|
|
983
|
-
chB.binaryType = 'arraybuffer';
|
|
984
|
-
const view = new Uint8Array([10, 20, 30]);
|
|
985
|
-
chA.send(view);
|
|
986
|
-
const msg = await withTimeout(5000, awaitMessage<ArrayBuffer>(chB), 'Uint8Array');
|
|
987
|
-
const arr = new Uint8Array(msg);
|
|
988
|
-
expect(arr.length).toBe(3);
|
|
989
|
-
expect(arr[0]).toBe(10);
|
|
990
|
-
expect(arr[2]).toBe(30);
|
|
991
|
-
} finally { closePeerConnections(pcA, pcB); }
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
await it('send on connecting channel throws', async () => {
|
|
995
|
-
const pc = new RTCPeerConnection();
|
|
996
|
-
try {
|
|
997
|
-
const ch = pc.createDataChannel('test');
|
|
998
|
-
expect(ch.readyState).toBe('connecting');
|
|
999
|
-
let threw = false;
|
|
1000
|
-
try { ch.send('data'); } catch { threw = true; }
|
|
1001
|
-
expect(threw).toBeTruthy();
|
|
1002
|
-
} finally { pc.close(); }
|
|
1003
|
-
});
|
|
1004
|
-
}
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
// ── RTCPeerConnection-setLocalDescription rollback ───────────────────
|
|
1008
|
-
// Ported from refs/wpt/webrtc/RTCPeerConnection-setLocalDescription-rollback.html
|
|
1009
|
-
|
|
1010
|
-
await describe('WPT — setLocalDescription rollback', async () => {
|
|
1011
|
-
if (!pipelineReady) {
|
|
1012
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
1013
|
-
expect(pipelineReady).toBeFalsy();
|
|
1014
|
-
});
|
|
1015
|
-
} else {
|
|
1016
|
-
await it('rollback from have-local-offer returns to stable', async () => {
|
|
1017
|
-
const pc = new RTCPeerConnection();
|
|
1018
|
-
try {
|
|
1019
|
-
pc.addTransceiver('audio');
|
|
1020
|
-
const offer = await pc.createOffer();
|
|
1021
|
-
await pc.setLocalDescription(offer);
|
|
1022
|
-
expect(pc.signalingState).toBe('have-local-offer');
|
|
1023
|
-
|
|
1024
|
-
await pc.setLocalDescription({ type: 'rollback', sdp: '' });
|
|
1025
|
-
expect(pc.signalingState).toBe('stable');
|
|
1026
|
-
} finally { pc.close(); }
|
|
1027
|
-
});
|
|
1028
|
-
|
|
1029
|
-
await it('rollback clears pendingLocalDescription', async () => {
|
|
1030
|
-
const pc = new RTCPeerConnection();
|
|
1031
|
-
try {
|
|
1032
|
-
pc.addTransceiver('audio');
|
|
1033
|
-
const offer = await pc.createOffer();
|
|
1034
|
-
await pc.setLocalDescription(offer);
|
|
1035
|
-
expect(pc.pendingLocalDescription).toBeDefined();
|
|
1036
|
-
|
|
1037
|
-
await pc.setLocalDescription({ type: 'rollback', sdp: '' });
|
|
1038
|
-
expect(pc.pendingLocalDescription).toBeNull();
|
|
1039
|
-
} finally { pc.close(); }
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
await it('rollback fires signalingstatechange', async () => {
|
|
1043
|
-
const pc = new RTCPeerConnection();
|
|
1044
|
-
try {
|
|
1045
|
-
pc.addTransceiver('audio');
|
|
1046
|
-
const offer = await pc.createOffer();
|
|
1047
|
-
await pc.setLocalDescription(offer);
|
|
1048
|
-
|
|
1049
|
-
let fired = false;
|
|
1050
|
-
pc.onsignalingstatechange = () => { fired = true; };
|
|
1051
|
-
await pc.setLocalDescription({ type: 'rollback', sdp: '' });
|
|
1052
|
-
expect(fired).toBeTruthy();
|
|
1053
|
-
} finally { pc.close(); }
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
await it('rollback in stable state is a no-op or throws', async () => {
|
|
1057
|
-
const pc = new RTCPeerConnection();
|
|
1058
|
-
try {
|
|
1059
|
-
expect(pc.signalingState).toBe('stable');
|
|
1060
|
-
// Per spec, rollback in stable may throw InvalidStateError
|
|
1061
|
-
// or be a no-op — both are acceptable
|
|
1062
|
-
try {
|
|
1063
|
-
await pc.setLocalDescription({ type: 'rollback', sdp: '' });
|
|
1064
|
-
} catch (e: any) {
|
|
1065
|
-
expect(e.name === 'InvalidStateError' || e instanceof Error).toBeTruthy();
|
|
1066
|
-
}
|
|
1067
|
-
expect(pc.signalingState).toBe('stable');
|
|
1068
|
-
} finally { pc.close(); }
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
// ── RTCPeerConnection-createAnswer validation ────────────────────────
|
|
1074
|
-
// Ported from refs/wpt/webrtc/RTCPeerConnection-createAnswer.html
|
|
1075
|
-
|
|
1076
|
-
await describe('WPT — RTCPeerConnection-createAnswer', async () => {
|
|
1077
|
-
if (!pipelineReady) {
|
|
1078
|
-
await it('(skipped — webrtcbin/nicesrc missing)', async () => {
|
|
1079
|
-
expect(pipelineReady).toBeFalsy();
|
|
1080
|
-
});
|
|
1081
|
-
} else {
|
|
1082
|
-
await it('createAnswer returns { type: "answer", sdp } in have-remote-offer', async () => {
|
|
1083
|
-
const pc1 = new RTCPeerConnection();
|
|
1084
|
-
const pc2 = new RTCPeerConnection();
|
|
1085
|
-
try {
|
|
1086
|
-
pc1.addTransceiver('audio');
|
|
1087
|
-
const offer = await pc1.createOffer();
|
|
1088
|
-
await pc2.setRemoteDescription(offer);
|
|
1089
|
-
expect(pc2.signalingState).toBe('have-remote-offer');
|
|
1090
|
-
|
|
1091
|
-
const answer = await pc2.createAnswer();
|
|
1092
|
-
expect(answer.type).toBe('answer');
|
|
1093
|
-
expect(typeof answer.sdp).toBe('string');
|
|
1094
|
-
expect(answer.sdp!.length).toBeGreaterThan(0);
|
|
1095
|
-
} finally { closePeerConnections(pc1, pc2); }
|
|
1096
|
-
});
|
|
1097
|
-
|
|
1098
|
-
await it('createAnswer rejects in stable state', async () => {
|
|
1099
|
-
const pc = new RTCPeerConnection();
|
|
1100
|
-
try {
|
|
1101
|
-
expect(pc.signalingState).toBe('stable');
|
|
1102
|
-
let threw = false;
|
|
1103
|
-
try { await pc.createAnswer(); } catch { threw = true; }
|
|
1104
|
-
expect(threw).toBeTruthy();
|
|
1105
|
-
} finally { pc.close(); }
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
await it('createAnswer SDP contains a= lines', async () => {
|
|
1109
|
-
const pc1 = new RTCPeerConnection();
|
|
1110
|
-
const pc2 = new RTCPeerConnection();
|
|
1111
|
-
try {
|
|
1112
|
-
pc1.addTransceiver('audio');
|
|
1113
|
-
const offer = await pc1.createOffer();
|
|
1114
|
-
await pc2.setRemoteDescription(offer);
|
|
1115
|
-
const answer = await pc2.createAnswer();
|
|
1116
|
-
// SDP must contain attribute lines
|
|
1117
|
-
expect(answer.sdp).toContain('a=');
|
|
1118
|
-
} finally { closePeerConnections(pc1, pc2); }
|
|
1119
|
-
});
|
|
1120
|
-
|
|
1121
|
-
await it('createAnswer rejects after close', async () => {
|
|
1122
|
-
const pc1 = new RTCPeerConnection();
|
|
1123
|
-
const pc2 = new RTCPeerConnection();
|
|
1124
|
-
try {
|
|
1125
|
-
pc1.addTransceiver('audio');
|
|
1126
|
-
const offer = await pc1.createOffer();
|
|
1127
|
-
await pc2.setRemoteDescription(offer);
|
|
1128
|
-
pc2.close();
|
|
1129
|
-
let threw = false;
|
|
1130
|
-
try { await pc2.createAnswer(); } catch { threw = true; }
|
|
1131
|
-
expect(threw).toBeTruthy();
|
|
1132
|
-
} finally { pc1.close(); }
|
|
1133
|
-
});
|
|
1134
|
-
}
|
|
1135
|
-
});
|
|
1136
|
-
};
|