summaryrefslogtreecommitdiffstats
path: root/dom/base/test/fullscreen/test_fullscreen-api-rapid-cycle.html
blob: 36e622ad4ca73b43a97996a454adc06ed604cb1b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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>