| <!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> |