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