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