blob: 28ae3afcd73e4100345edddc505895add26ea56d [file] [log] [blame]
<!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>