Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!DOCTYPE html>
<html>
<head>
<title>Test for rapid cycling of Fullscreen API requests</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<script>
// There are two ways that web content should be able to reliably
// request and respond to fullscreen:
//
// 1) Wait on the requestFullscreen() and exitFullscreen() promises.
// 2) Respond to the "fullscreenchange" and "fullscreenerror" events
// after calling requestFullscreen() or exitFullscreen().
//
// This test exercises both methods rapidly, while checking to see
// if any expected signal is taking too long. If awaiting a promise
// or waiting for an event takes longer than some number of seconds,
// the test will fail instead of timing out. This is to help detect
// vulnerabilities in the implementation which would slow down the
// test harness waiting for the timeout.
// How many enter-exit cycles we run for each method of detecting a
// fullscreen transition.
const CYCLE_COUNT = 3;
// How long do we wait for one transition before considering it as
// an error.
const TOO_LONG_SECONDS = 3;
SimpleTest.requestFlakyTimeout("We race against Promises to turn possible timeouts into errors.");
function rejectAfterTooLong() {
return new Promise((resolve, reject) => {
const fail = () => {
reject(`timeout after ${TOO_LONG_SECONDS} seconds`);
}
setTimeout(fail, TOO_LONG_SECONDS * 1000);
});
}
add_setup(async () => {
await SpecialPowers.pushPrefEnv({
"set": [
// Keep the test structure simple.
["full-screen-api.allow-trusted-requests-only", false],
// Make macOS fullscreen transitions asynchronous.
["full-screen-api.macos-native-full-screen", true],
// Clarify that even no-duration async transitions are vulnerable.
["full-screen-api.transition-duration.enter", "0 0"],
["full-screen-api.transition-duration.leave", "0 0"],
]
});
});
add_task(ensureOutOfFullscreen);
// It is an implementation detail that promises resolve first, and
// then events are fired on a later event loop. For this reason,
// it's very important that we do the rapidCycleAwaitEvents task
// first, because we don't want to have any "stray" fullscreenchange
// events in the pipeline when we start that task. Conversely,
// there's really no way for the rapidCycleAwaitEvents to poison
// the environment for the next task, which waits on promises.
add_task(rapidCycleAwaitEvents);
add_task(ensureOutOfFullscreen);
add_task(rapidCycleAwaitPromises);
add_task(() => { ok(true, "Completed test with one expected result."); });
// This is a helper function to repeatedly invoke a Promise generator
// until the Promise resolves, delaying by one event loop on each
// attempt.
async function repeatUntilSuccessful(f) {
let successful = false;
do {
try {
// Delay one event loop.
await new Promise(r => SimpleTest.executeSoon(r));
await f();
successful = true;
} catch (error) {
info(`repeatUntilSuccessful: error ${error}.`);
}
} while(!successful);
}
async function ensureOutOfFullscreen() {
// Repeatedly call exitFullscreen until we get out.
await repeatUntilSuccessful(async () => {
if (document.fullscreenElement) {
await document.exitFullscreen();
}
if (document.fullscreenElement) {
throw new Error("still in fullscreen");
}
});
}
async function rapidCycleAwaitEvents() {
const receiveOneFullscreenchange = () => {
return new Promise(resolve => {
document.addEventListener("fullscreenchange", resolve, { once: true });
});
};
let gotError = false;
for (let cycle = 0; cycle < CYCLE_COUNT; cycle++) {
info(`Event cycle ${cycle} request fullscreen.`);
const enterPromise = receiveOneFullscreenchange();
document.documentElement.requestFullscreen();
await Promise.race([enterPromise, rejectAfterTooLong()]).catch(error => {
ok(false, `Event cycle ${cycle} requestFullscreen errored with ${error}.`);
gotError = true;
});
if (gotError) {
break;
}
info(`Event cycle ${cycle} exit fullscreen.`);
const exitPromise = receiveOneFullscreenchange();
document.exitFullscreen();
await Promise.race([exitPromise, rejectAfterTooLong()]).catch(error => {
ok(false, `Event cycle ${cycle} exitFullscreen errored with ${error}.`);
gotError = true;
});
if (gotError) {
break;
}
}
}
async function rapidCycleAwaitPromises() {
let gotError = false;
for (let cycle = 0; cycle < CYCLE_COUNT; cycle++) {
info(`Promise cycle ${cycle} request fullscreen.`);
const enterPromise = document.documentElement.requestFullscreen();
await Promise.race([enterPromise, rejectAfterTooLong()]).catch(error => {
ok(false, `Promise cycle ${cycle} requestFullscreen errored with ${error}.`);
gotError = true;
});
if (gotError) {
break;
}
info(`Promise cycle ${cycle} exit fullscreen.`);
const exitPromise = document.exitFullscreen();
await Promise.race([exitPromise, rejectAfterTooLong()]).catch(error => {
ok(false, `Promise cycle ${cycle} exitFullscreen errored with ${error}.`);
gotError = true;
});
if (gotError) {
break;
}
}
}
</script>
</body>
</html>