summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webrtc/RTCPeerConnection-operations.https.html
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-operations.https.html425
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>