RTCPeerConnection-operations.https.html (15650B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title></title> 4 <script src=/resources/testharness.js></script> 5 <script src=/resources/testharnessreport.js></script> 6 <script> 7 'use strict'; 8 9 // Helpers to test APIs "return a promise rejected with a newly created" error. 10 // Strictly speaking this means already-rejected upon return. 11 function promiseState(p) { 12 const t = {}; 13 return Promise.race([p, t]) 14 .then(v => (v === t)? "pending" : "fulfilled", () => "rejected"); 15 } 16 17 // However, to allow promises to be used in implementations, this helper adds 18 // some slack: returning a pending promise will pass, provided it is rejected 19 // before the end of the current run of the event loop (i.e. on microtask queue 20 // before next task). 21 async function promiseStateFinal(p) { 22 for (let i = 0; i < 20; i++) { 23 await promiseState(p); 24 } 25 return promiseState(p); 26 } 27 28 [promiseState, promiseStateFinal].forEach(f => promise_test(async t => { 29 assert_equals(await f(Promise.resolve()), "fulfilled"); 30 assert_equals(await f(Promise.reject()), "rejected"); 31 assert_equals(await f(new Promise(() => {})), "pending"); 32 }, `${f.name} helper works`)); 33 34 promise_test(async t => { 35 const pc = new RTCPeerConnection(); 36 t.add_cleanup(() => pc.close()); 37 await pc.setRemoteDescription(await pc.createOffer()); 38 const p = pc.createOffer(); 39 const haveState = promiseStateFinal(p); 40 try { 41 await p; 42 assert_unreached("Control. Must not succeed"); 43 } catch (e) { 44 assert_equals(e.name, "InvalidStateError"); 45 } 46 assert_equals(await haveState, "rejected", "promise rejected on same task"); 47 }, "createOffer must detect InvalidStateError synchronously when chain is empty (prerequisite)"); 48 49 promise_test(async t => { 50 const pc = new RTCPeerConnection(); 51 t.add_cleanup(() => pc.close()); 52 const p = pc.createAnswer(); 53 const haveState = promiseStateFinal(p); 54 try { 55 await p; 56 assert_unreached("Control. Must not succeed"); 57 } catch (e) { 58 assert_equals(e.name, "InvalidStateError"); 59 } 60 assert_equals(await haveState, "rejected", "promise rejected on same task"); 61 }, "createAnswer must detect InvalidStateError synchronously when chain is empty (prerequisite)"); 62 63 promise_test(async t => { 64 const pc = new RTCPeerConnection(); 65 t.add_cleanup(() => pc.close()); 66 const p = pc.setLocalDescription({type: "rollback"}); 67 const haveState = promiseStateFinal(p); 68 try { 69 await p; 70 assert_unreached("Control. Must not succeed"); 71 } catch (e) { 72 assert_equals(e.name, "InvalidStateError"); 73 } 74 assert_equals(await haveState, "rejected", "promise rejected on same task"); 75 }, "SLD(rollback) must detect InvalidStateError synchronously when chain is empty"); 76 77 promise_test(async t => { 78 const pc = new RTCPeerConnection(); 79 t.add_cleanup(() => pc.close()); 80 const p = pc.addIceCandidate(); 81 const haveState = promiseStateFinal(p); 82 try { 83 await p; 84 assert_unreached("Control. Must not succeed"); 85 } catch (e) { 86 assert_equals(e.name, "InvalidStateError"); 87 } 88 assert_equals(pc.remoteDescription, null, "no remote desciption"); 89 assert_equals(await haveState, "rejected", "promise rejected on same task"); 90 }, "addIceCandidate must detect InvalidStateError synchronously when chain is empty"); 91 92 promise_test(async t => { 93 const pc = new RTCPeerConnection(); 94 t.add_cleanup(() => pc.close()); 95 const transceiver = pc.addTransceiver("audio"); 96 transceiver.stop(); 97 const p = transceiver.sender.replaceTrack(null); 98 const haveState = promiseStateFinal(p); 99 try { 100 await p; 101 assert_unreached("Control. Must not succeed"); 102 } catch (e) { 103 assert_equals(e.name, "InvalidStateError"); 104 } 105 assert_equals(await haveState, "rejected", "promise rejected on same task"); 106 }, "replaceTrack must detect InvalidStateError synchronously when chain is empty and transceiver is stopped"); 107 108 promise_test(async t => { 109 const pc = new RTCPeerConnection(); 110 t.add_cleanup(() => pc.close()); 111 const transceiver = pc.addTransceiver("audio"); 112 transceiver.stop(); 113 const parameters = transceiver.sender.getParameters(); 114 const p = transceiver.sender.setParameters(parameters); 115 const haveState = promiseStateFinal(p); 116 try { 117 await p; 118 assert_unreached("Control. Must not succeed"); 119 } catch (e) { 120 assert_equals(e.name, "InvalidStateError"); 121 } 122 assert_equals(await haveState, "rejected", "promise rejected on same task"); 123 }, "setParameters must detect InvalidStateError synchronously always when transceiver is stopped"); 124 125 promise_test(async t => { 126 const pc = new RTCPeerConnection(); 127 t.add_cleanup(() => pc.close()); 128 const {track} = new RTCPeerConnection().addTransceiver("audio").receiver; 129 assert_not_equals(track, null); 130 const p = pc.getStats(track); 131 const haveState = promiseStateFinal(p); 132 try { 133 await p; 134 assert_unreached("Control. Must not succeed"); 135 } catch (e) { 136 assert_equals(e.name, "InvalidAccessError"); 137 } 138 assert_equals(await haveState, "rejected", "promise rejected on same task"); 139 }, "pc.getStats must detect InvalidAccessError synchronously always"); 140 141 // Helper builds on above tests to check if operations queue is empty or not. 142 // 143 // Meaning of "empty": Because this helper uses the sloppy promiseStateFinal, 144 // it may not detect operations on the chain unless they block the current run 145 // of the event loop. In other words, it may not detect operations on the chain 146 // that resolve on the emptying of the microtask queue at the end of this run of 147 // the event loop. 148 149 async function isOperationsChainEmpty(pc) { 150 let p, error; 151 const signalingState = pc.signalingState; 152 if (signalingState == "have-remote-offer") { 153 p = pc.createOffer(); 154 } else { 155 p = pc.createAnswer(); 156 } 157 const state = await promiseStateFinal(p); 158 try { 159 await p; 160 // This helper tries to avoid side-effects by always failing, 161 // but createAnswer above may succeed if chained after an SRD 162 // that changes the signaling state on us. Ignore that success. 163 if (signalingState == pc.signalingState) { 164 assert_unreached("Control. Must not succeed"); 165 } 166 } catch (e) { 167 assert_equals(e.name, "InvalidStateError", 168 "isOperationsChainEmpty is working"); 169 } 170 return state == "rejected"; 171 } 172 173 promise_test(async t => { 174 const pc = new RTCPeerConnection(); 175 t.add_cleanup(() => pc.close()); 176 assert_true(await isOperationsChainEmpty(pc), "Empty to start"); 177 }, "isOperationsChainEmpty detects empty in stable"); 178 179 promise_test(async t => { 180 const pc = new RTCPeerConnection(); 181 t.add_cleanup(() => pc.close()); 182 await pc.setLocalDescription(await pc.createOffer()); 183 assert_true(await isOperationsChainEmpty(pc), "Empty to start"); 184 }, "isOperationsChainEmpty detects empty in have-local-offer"); 185 186 promise_test(async t => { 187 const pc = new RTCPeerConnection(); 188 t.add_cleanup(() => pc.close()); 189 await pc.setRemoteDescription(await pc.createOffer()); 190 assert_true(await isOperationsChainEmpty(pc), "Empty to start"); 191 }, "isOperationsChainEmpty detects empty in have-remote-offer"); 192 193 promise_test(async t => { 194 const pc = new RTCPeerConnection(); 195 t.add_cleanup(() => pc.close()); 196 const p = pc.createOffer(); 197 assert_false(await isOperationsChainEmpty(pc), "Non-empty chain"); 198 await p; 199 }, "createOffer uses operations chain"); 200 201 promise_test(async t => { 202 const pc = new RTCPeerConnection(); 203 t.add_cleanup(() => pc.close()); 204 await pc.setRemoteDescription(await pc.createOffer()); 205 const p = pc.createAnswer(); 206 assert_false(await isOperationsChainEmpty(pc), "Non-empty chain"); 207 await p; 208 }, "createAnswer uses operations chain"); 209 210 promise_test(async t => { 211 const pc = new RTCPeerConnection(); 212 t.add_cleanup(() => pc.close()); 213 const offer = await pc.createOffer(); 214 assert_true(await isOperationsChainEmpty(pc), "Empty before"); 215 const p = pc.setLocalDescription(offer); 216 assert_false(await isOperationsChainEmpty(pc), "Non-empty chain"); 217 await p; 218 }, "setLocalDescription uses operations chain"); 219 220 promise_test(async t => { 221 const pc = new RTCPeerConnection(); 222 t.add_cleanup(() => pc.close()); 223 const offer = await pc.createOffer(); 224 assert_true(await isOperationsChainEmpty(pc), "Empty before"); 225 const p = pc.setRemoteDescription(offer); 226 assert_false(await isOperationsChainEmpty(pc), "Non-empty chain"); 227 await p; 228 }, "setRemoteDescription uses operations chain"); 229 230 promise_test(async t => { 231 const pc1 = new RTCPeerConnection(); 232 t.add_cleanup(() => pc1.close()); 233 const pc2 = new RTCPeerConnection(); 234 t.add_cleanup(() => pc2.close()); 235 236 pc1.addTransceiver("video"); 237 const offer = await pc1.createOffer(); 238 await pc1.setLocalDescription(offer); 239 const {candidate} = await new Promise(r => pc1.onicecandidate = r); 240 await pc2.setRemoteDescription(offer); 241 const p = pc2.addIceCandidate(candidate); 242 assert_false(await isOperationsChainEmpty(pc2), "Non-empty chain"); 243 await p; 244 }, "addIceCandidate uses operations chain"); 245 246 promise_test(async t => { 247 const pc = new RTCPeerConnection(); 248 t.add_cleanup(() => pc.close()); 249 const transceiver = pc.addTransceiver("audio"); 250 await new Promise(r => pc.onnegotiationneeded = r); 251 assert_true(await isOperationsChainEmpty(pc), "Empty chain"); 252 await new Promise(r => t.step_timeout(r, 0)); 253 assert_true(await isOperationsChainEmpty(pc), "Empty chain"); 254 }, "Firing of negotiationneeded does NOT use operations chain"); 255 256 promise_test(async t => { 257 const pc1 = new RTCPeerConnection(); 258 t.add_cleanup(() => pc1.close()); 259 const pc2 = new RTCPeerConnection(); 260 t.add_cleanup(() => pc2.close()); 261 262 pc1.addTransceiver("audio"); 263 pc1.addTransceiver("video"); 264 const offer = await pc1.createOffer(); 265 await pc1.setLocalDescription(offer); 266 const candidates = []; 267 for (let c; (c = (await new Promise(r => pc1.onicecandidate = r)).candidate);) { 268 candidates.push(c); 269 } 270 pc2.addTransceiver("video"); 271 let fired = false; 272 const p = new Promise(r => pc2.onnegotiationneeded = () => r(fired = true)); 273 await Promise.all([ 274 pc2.setRemoteDescription(offer), 275 ...candidates.map(candidate => pc2.addIceCandidate(candidate)), 276 pc2.setLocalDescription() 277 ]); 278 assert_false(fired, "Negotiationneeded mustn't have fired yet."); 279 await new Promise(r => t.step_timeout(r, 0)); 280 assert_true(fired, "Negotiationneeded must have fired by now."); 281 await p; 282 }, "Negotiationneeded only fires once operations chain is empty"); 283 284 promise_test(async t => { 285 const pc = new RTCPeerConnection(); 286 t.add_cleanup(() => pc.close()); 287 const transceiver = pc.addTransceiver("audio"); 288 await new Promise(r => pc.onnegotiationneeded = r); 289 // Note: since the negotiationneeded event is fired from a chained synchronous 290 // function in the spec, queue a task before doing our precheck. 291 await new Promise(r => t.step_timeout(r, 0)); 292 assert_true(await isOperationsChainEmpty(pc), "Empty chain"); 293 const p = transceiver.sender.replaceTrack(null); 294 assert_false(await isOperationsChainEmpty(pc), "Non-empty chain"); 295 await p; 296 }, "replaceTrack uses operations chain"); 297 298 promise_test(async t => { 299 const pc = new RTCPeerConnection(); 300 t.add_cleanup(() => pc.close()); 301 const transceiver = pc.addTransceiver("audio"); 302 await new Promise(r => pc.onnegotiationneeded = r); 303 await new Promise(r => t.step_timeout(r, 0)); 304 assert_true(await isOperationsChainEmpty(pc), "Empty chain"); 305 const parameters = transceiver.sender.getParameters(); 306 const p = transceiver.sender.setParameters(parameters); 307 const haveState = promiseStateFinal(p); 308 assert_true(await isOperationsChainEmpty(pc), "Empty chain"); 309 assert_equals(await haveState, "pending", "Method is async"); 310 await p; 311 }, "setParameters does NOT use the operations chain"); 312 313 promise_test(async t => { 314 const pc = new RTCPeerConnection(); 315 t.add_cleanup(() => pc.close()); 316 const p = pc.getStats(); 317 const haveState = promiseStateFinal(p); 318 assert_true(await isOperationsChainEmpty(pc), "Empty chain"); 319 assert_equals(await haveState, "pending", "Method is async"); 320 await p; 321 }, "pc.getStats does NOT use the operations chain"); 322 323 promise_test(async t => { 324 const pc = new RTCPeerConnection(); 325 t.add_cleanup(() => pc.close()); 326 const {sender} = pc.addTransceiver("audio"); 327 await new Promise(r => pc.onnegotiationneeded = r); 328 await new Promise(r => t.step_timeout(r, 0)); 329 assert_true(await isOperationsChainEmpty(pc), "Empty chain"); 330 const p = sender.getStats(); 331 const haveState = promiseStateFinal(p); 332 assert_true(await isOperationsChainEmpty(pc), "Empty chain"); 333 assert_equals(await haveState, "pending", "Method is async"); 334 await p; 335 }, "sender.getStats does NOT use the operations chain"); 336 337 promise_test(async t => { 338 const pc = new RTCPeerConnection(); 339 t.add_cleanup(() => pc.close()); 340 const {receiver} = pc.addTransceiver("audio"); 341 await new Promise(r => pc.onnegotiationneeded = r); 342 await new Promise(r => t.step_timeout(r, 0)); 343 assert_true(await isOperationsChainEmpty(pc), "Empty chain"); 344 const p = receiver.getStats(); 345 const haveState = promiseStateFinal(p); 346 assert_true(await isOperationsChainEmpty(pc), "Empty chain"); 347 assert_equals(await haveState, "pending", "Method is async"); 348 await p; 349 }, "receiver.getStats does NOT use the operations chain"); 350 351 promise_test(async t => { 352 const pc1 = new RTCPeerConnection(); 353 t.add_cleanup(() => pc1.close()); 354 const pc2 = new RTCPeerConnection(); 355 t.add_cleanup(() => pc2.close()); 356 357 pc1.addTransceiver("video"); 358 const offer = await pc1.createOffer(); 359 await pc1.setLocalDescription(offer); 360 const {candidate} = await new Promise(r => pc1.onicecandidate = r); 361 try { 362 await pc2.addIceCandidate(candidate); 363 assert_unreached("Control. Must not succeed"); 364 } catch (e) { 365 assert_equals(e.name, "InvalidStateError"); 366 } 367 const p = pc2.setRemoteDescription(offer); 368 await pc2.addIceCandidate(candidate); 369 await p; 370 }, "addIceCandidate chains onto SRD, fails before"); 371 372 promise_test(async t => { 373 const pc = new RTCPeerConnection(); 374 t.add_cleanup(() => pc.close()); 375 376 const offer = await pc.createOffer(); 377 pc.addTransceiver("video"); 378 await new Promise(r => pc.onnegotiationneeded = r); 379 const p = (async () => { 380 await pc.setLocalDescription(); 381 })(); 382 await new Promise(r => t.step_timeout(r, 0)); 383 await pc.setRemoteDescription(offer); 384 await p; 385 }, "Operations queue not vulnerable to recursion by chained negotiationneeded"); 386 387 promise_test(async t => { 388 const pc1 = new RTCPeerConnection(); 389 t.add_cleanup(() => pc1.close()); 390 const pc2 = new RTCPeerConnection(); 391 t.add_cleanup(() => pc2.close()); 392 393 pc1.addTransceiver("video"); 394 await Promise.all([ 395 pc1.createOffer(), 396 pc1.setLocalDescription({type: "offer"}) 397 ]); 398 await Promise.all([ 399 pc2.setRemoteDescription(pc1.localDescription), 400 pc2.createAnswer(), 401 pc2.setLocalDescription({type: "answer"}) 402 ]); 403 await pc1.setRemoteDescription(pc2.localDescription); 404 }, "Pack operations queue with implicit offer and answer"); 405 406 promise_test(async t => { 407 const pc1 = new RTCPeerConnection(); 408 t.add_cleanup(() => pc1.close()); 409 const pc2 = new RTCPeerConnection(); 410 t.add_cleanup(() => pc2.close()); 411 412 const state = (pc, s) => new Promise(r => pc.onsignalingstatechange = 413 () => pc.signalingState == s && r()); 414 pc1.addTransceiver("video"); 415 pc1.createOffer(); 416 pc1.setLocalDescription({type: "offer"}); 417 await state(pc1, "have-local-offer"); 418 pc2.setRemoteDescription(pc1.localDescription); 419 pc2.createAnswer(); 420 pc2.setLocalDescription({type: "answer"}); 421 await state(pc2, "stable"); 422 await pc1.setRemoteDescription(pc2.localDescription); 423 }, "Negotiate solely by operations queue and signaling state"); 424 425 </script>