summaryrefslogtreecommitdiffstats
path: root/dom/base/test/fullscreen/test_fullscreen-api-rapid-cycle.html
diff options
context:
space:
mode:
Diffstat (limited to 'dom/base/test/fullscreen/test_fullscreen-api-rapid-cycle.html')
-rw-r--r--dom/base/test/fullscreen/test_fullscreen-api-rapid-cycle.html167
1 files changed, 167 insertions, 0 deletions
diff --git a/dom/base/test/fullscreen/test_fullscreen-api-rapid-cycle.html b/dom/base/test/fullscreen/test_fullscreen-api-rapid-cycle.html
new file mode 100644
index 0000000000..36e622ad4c
--- /dev/null
+++ b/dom/base/test/fullscreen/test_fullscreen-api-rapid-cycle.html
@@ -0,0 +1,167 @@
+<!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>