diff options
Diffstat (limited to '')
-rw-r--r-- | testing/web-platform/tests/webrtc/RTCPeerConnection-operations.https.html | 425 |
1 files changed, 425 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-operations.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-operations.https.html new file mode 100644 index 0000000000..28ae3afcd7 --- /dev/null +++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-operations.https.html @@ -0,0 +1,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> |