summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webrtc/RTCPeerConnection-operations.https.html
blob: 28ae3afcd73e4100345edddc505895add26ea56d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
<!doctype html>
<meta charset=utf-8>
<title></title>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script>
'use strict';

// Helpers to test APIs "return a promise rejected with a newly created" error.
// Strictly speaking this means already-rejected upon return.
function promiseState(p) {
  const t = {};
  return Promise.race([p, t])
    .then(v => (v === t)? "pending" : "fulfilled", () => "rejected");
}

// However, to allow promises to be used in implementations, this helper adds
// some slack: returning a pending promise will pass, provided it is rejected
// before the end of the current run of the event loop (i.e. on microtask queue
// before next task).
async function promiseStateFinal(p) {
  for (let i = 0; i < 20; i++) {
    await promiseState(p);
  }
  return promiseState(p);
}

[promiseState, promiseStateFinal].forEach(f => promise_test(async t => {
  assert_equals(await f(Promise.resolve()), "fulfilled");
  assert_equals(await f(Promise.reject()), "rejected");
  assert_equals(await f(new Promise(() => {})), "pending");
}, `${f.name} helper works`));

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  await pc.setRemoteDescription(await pc.createOffer());
  const p = pc.createOffer();
  const haveState = promiseStateFinal(p);
  try {
    await p;
    assert_unreached("Control. Must not succeed");
  } catch (e) {
    assert_equals(e.name, "InvalidStateError");
  }
  assert_equals(await haveState, "rejected", "promise rejected on same task");
}, "createOffer must detect InvalidStateError synchronously when chain is empty (prerequisite)");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const p = pc.createAnswer();
  const haveState = promiseStateFinal(p);
  try {
    await p;
    assert_unreached("Control. Must not succeed");
  } catch (e) {
    assert_equals(e.name, "InvalidStateError");
  }
  assert_equals(await haveState, "rejected", "promise rejected on same task");
}, "createAnswer must detect InvalidStateError synchronously when chain is empty (prerequisite)");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const p = pc.setLocalDescription({type: "rollback"});
  const haveState = promiseStateFinal(p);
  try {
    await p;
    assert_unreached("Control. Must not succeed");
  } catch (e) {
    assert_equals(e.name, "InvalidStateError");
  }
  assert_equals(await haveState, "rejected", "promise rejected on same task");
}, "SLD(rollback) must detect InvalidStateError synchronously when chain is empty");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const p = pc.addIceCandidate();
  const haveState = promiseStateFinal(p);
  try {
    await p;
    assert_unreached("Control. Must not succeed");
  } catch (e) {
    assert_equals(e.name, "InvalidStateError");
  }
  assert_equals(pc.remoteDescription, null, "no remote desciption");
  assert_equals(await haveState, "rejected", "promise rejected on same task");
}, "addIceCandidate must detect InvalidStateError synchronously when chain is empty");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const transceiver = pc.addTransceiver("audio");
  transceiver.stop();
  const p = transceiver.sender.replaceTrack(null);
  const haveState = promiseStateFinal(p);
  try {
    await p;
    assert_unreached("Control. Must not succeed");
  } catch (e) {
    assert_equals(e.name, "InvalidStateError");
  }
  assert_equals(await haveState, "rejected", "promise rejected on same task");
}, "replaceTrack must detect InvalidStateError synchronously when chain is empty and transceiver is stopped");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const transceiver = pc.addTransceiver("audio");
  transceiver.stop();
  const parameters = transceiver.sender.getParameters();
  const p = transceiver.sender.setParameters(parameters);
  const haveState = promiseStateFinal(p);
  try {
    await p;
    assert_unreached("Control. Must not succeed");
  } catch (e) {
    assert_equals(e.name, "InvalidStateError");
  }
  assert_equals(await haveState, "rejected", "promise rejected on same task");
}, "setParameters must detect InvalidStateError synchronously always when transceiver is stopped");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const {track} = new RTCPeerConnection().addTransceiver("audio").receiver;
  assert_not_equals(track, null);
  const p = pc.getStats(track);
  const haveState = promiseStateFinal(p);
  try {
    await p;
    assert_unreached("Control. Must not succeed");
  } catch (e) {
    assert_equals(e.name, "InvalidAccessError");
  }
  assert_equals(await haveState, "rejected", "promise rejected on same task");
}, "pc.getStats must detect InvalidAccessError synchronously always");

// Helper builds on above tests to check if operations queue is empty or not.
//
// Meaning of "empty": Because this helper uses the sloppy promiseStateFinal,
// it may not detect operations on the chain unless they block the current run
// of the event loop. In other words, it may not detect operations on the chain
// that resolve on the emptying of the microtask queue at the end of this run of
// the event loop.

async function isOperationsChainEmpty(pc) {
  let p, error;
  const signalingState = pc.signalingState;
  if (signalingState == "have-remote-offer") {
    p = pc.createOffer();
  } else {
    p = pc.createAnswer();
  }
  const state = await promiseStateFinal(p);
  try {
    await p;
    // This helper tries to avoid side-effects by always failing,
    // but createAnswer above may succeed if chained after an SRD
    // that changes the signaling state on us. Ignore that success.
    if (signalingState == pc.signalingState) {
      assert_unreached("Control. Must not succeed");
    }
  } catch (e) {
    assert_equals(e.name, "InvalidStateError",
                  "isOperationsChainEmpty is working");
  }
  return state == "rejected";
}

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  assert_true(await isOperationsChainEmpty(pc), "Empty to start");
}, "isOperationsChainEmpty detects empty in stable");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  await pc.setLocalDescription(await pc.createOffer());
  assert_true(await isOperationsChainEmpty(pc), "Empty to start");
}, "isOperationsChainEmpty detects empty in have-local-offer");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  await pc.setRemoteDescription(await pc.createOffer());
  assert_true(await isOperationsChainEmpty(pc), "Empty to start");
}, "isOperationsChainEmpty detects empty in have-remote-offer");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const p = pc.createOffer();
  assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
  await p;
}, "createOffer uses operations chain");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  await pc.setRemoteDescription(await pc.createOffer());
  const p = pc.createAnswer();
  assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
  await p;
}, "createAnswer uses operations chain");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const offer = await pc.createOffer();
  assert_true(await isOperationsChainEmpty(pc), "Empty before");
  const p = pc.setLocalDescription(offer);
  assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
  await p;
}, "setLocalDescription uses operations chain");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const offer = await pc.createOffer();
  assert_true(await isOperationsChainEmpty(pc), "Empty before");
  const p = pc.setRemoteDescription(offer);
  assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
  await p;
}, "setRemoteDescription uses operations chain");

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  pc1.addTransceiver("video");
  const offer = await pc1.createOffer();
  await pc1.setLocalDescription(offer);
  const {candidate} = await new Promise(r => pc1.onicecandidate = r);
  await pc2.setRemoteDescription(offer);
  const p = pc2.addIceCandidate(candidate);
  assert_false(await isOperationsChainEmpty(pc2), "Non-empty chain");
  await p;
}, "addIceCandidate uses operations chain");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const transceiver = pc.addTransceiver("audio");
  await new Promise(r => pc.onnegotiationneeded = r);
  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
  await new Promise(r => t.step_timeout(r, 0));
  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
}, "Firing of negotiationneeded does NOT use operations chain");

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  pc1.addTransceiver("audio");
  pc1.addTransceiver("video");
  const offer = await pc1.createOffer();
  await pc1.setLocalDescription(offer);
  const candidates = [];
  for (let c; (c = (await new Promise(r => pc1.onicecandidate = r)).candidate);) {
    candidates.push(c);
  }
  pc2.addTransceiver("video");
  let fired = false;
  const p = new Promise(r => pc2.onnegotiationneeded = () => r(fired = true));
  await Promise.all([
    pc2.setRemoteDescription(offer),
    ...candidates.map(candidate => pc2.addIceCandidate(candidate)),
    pc2.setLocalDescription()
  ]);
  assert_false(fired, "Negotiationneeded mustn't have fired yet.");
  await new Promise(r => t.step_timeout(r, 0));
  assert_true(fired, "Negotiationneeded must have fired by now.");
  await p;
}, "Negotiationneeded only fires once operations chain is empty");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const transceiver = pc.addTransceiver("audio");
  await new Promise(r => pc.onnegotiationneeded = r);
  // Note: since the negotiationneeded event is fired from a chained synchronous
  // function in the spec, queue a task before doing our precheck.
  await new Promise(r => t.step_timeout(r, 0));
  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
  const p = transceiver.sender.replaceTrack(null);
  assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
  await p;
}, "replaceTrack uses operations chain");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const transceiver = pc.addTransceiver("audio");
  await new Promise(r => pc.onnegotiationneeded = r);
  await new Promise(r => t.step_timeout(r, 0));
  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
  const parameters = transceiver.sender.getParameters();
  const p = transceiver.sender.setParameters(parameters);
  const haveState = promiseStateFinal(p);
  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
  assert_equals(await haveState, "pending", "Method is async");
  await p;
}, "setParameters does NOT use the operations chain");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const p = pc.getStats();
  const haveState = promiseStateFinal(p);
  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
  assert_equals(await haveState, "pending", "Method is async");
  await p;
}, "pc.getStats does NOT use the operations chain");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const {sender} = pc.addTransceiver("audio");
  await new Promise(r => pc.onnegotiationneeded = r);
  await new Promise(r => t.step_timeout(r, 0));
  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
  const p = sender.getStats();
  const haveState = promiseStateFinal(p);
  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
  assert_equals(await haveState, "pending", "Method is async");
  await p;
}, "sender.getStats does NOT use the operations chain");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());
  const {receiver} = pc.addTransceiver("audio");
  await new Promise(r => pc.onnegotiationneeded = r);
  await new Promise(r => t.step_timeout(r, 0));
  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
  const p = receiver.getStats();
  const haveState = promiseStateFinal(p);
  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
  assert_equals(await haveState, "pending", "Method is async");
  await p;
}, "receiver.getStats does NOT use the operations chain");

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  pc1.addTransceiver("video");
  const offer = await pc1.createOffer();
  await pc1.setLocalDescription(offer);
  const {candidate} = await new Promise(r => pc1.onicecandidate = r);
  try {
    await pc2.addIceCandidate(candidate);
    assert_unreached("Control. Must not succeed");
  } catch (e) {
    assert_equals(e.name, "InvalidStateError");
  }
  const p = pc2.setRemoteDescription(offer);
  await pc2.addIceCandidate(candidate);
  await p;
}, "addIceCandidate chains onto SRD, fails before");

promise_test(async t => {
  const pc = new RTCPeerConnection();
  t.add_cleanup(() => pc.close());

  const offer = await pc.createOffer();
  pc.addTransceiver("video");
  await new Promise(r => pc.onnegotiationneeded = r);
  const p = (async () => {
    await pc.setLocalDescription();
  })();
  await new Promise(r => t.step_timeout(r, 0));
  await pc.setRemoteDescription(offer);
  await p;
}, "Operations queue not vulnerable to recursion by chained negotiationneeded");

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  pc1.addTransceiver("video");
  await Promise.all([
    pc1.createOffer(),
    pc1.setLocalDescription({type: "offer"})
  ]);
  await Promise.all([
    pc2.setRemoteDescription(pc1.localDescription),
    pc2.createAnswer(),
    pc2.setLocalDescription({type: "answer"})
  ]);
  await pc1.setRemoteDescription(pc2.localDescription);
}, "Pack operations queue with implicit offer and answer");

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  const state = (pc, s) => new Promise(r => pc.onsignalingstatechange =
                                       () => pc.signalingState == s && r());
  pc1.addTransceiver("video");
  pc1.createOffer();
  pc1.setLocalDescription({type: "offer"});
  await state(pc1, "have-local-offer");
  pc2.setRemoteDescription(pc1.localDescription);
  pc2.createAnswer();
  pc2.setLocalDescription({type: "answer"});
  await state(pc2, "stable");
  await pc1.setRemoteDescription(pc2.localDescription);
}, "Negotiate solely by operations queue and signaling state");

</script>