mirror of
https://github.com/servo/servo.git
synced 2025-06-25 09:34:32 +01:00
425 lines
15 KiB
HTML
425 lines
15 KiB
HTML
<!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>
|