Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Errors
- This test failed 6 times in the preceding 30 days. quicksearch this test
- Manifest: dom/base/test/fullscreen/mochitest.toml
<!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>