summaryrefslogtreecommitdiffstats
path: root/dom/media/mediacontrol/tests/browser/browser_suspend_inactive_tab.js
blob: 334717a2f29c6d766d0e19d280122b10f9707e70 (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
const PAGE_NON_AUTOPLAY =
  "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
const VIDEO_ID = "video";

add_task(async function setupTestingPref() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["media.mediacontrol.testingevents.enabled", true],
      ["dom.suspend_inactive.enabled", true],
      ["dom.audiocontext.testing", true],
    ],
  });
});

/**
 * This test to used to test the feature that would suspend the inactive tab,
 * which currently is only used on Android.
 *
 * Normally when tab becomes inactive, we would suspend it and stop its script
 * from running. However, if a tab has a main controller, which indicates it
 * might have playng media, or waiting media keys to control media, then it
 * would not be suspended event if it's inactive.
 *
 * In addition, Note that, on Android, audio focus management is enabled by
 * default, so there is only one tab being able to play at a time, which means
 * the tab playing media always has main controller.
 */
add_task(async function testInactiveTabWouldBeSuspended() {
  info(`open a tab`);
  const tab = await createTab(PAGE_NON_AUTOPLAY);
  await assertIfWindowGetSuspended(tab, { shouldBeSuspended: false });

  info(`tab should be suspended when it becomes inactive`);
  setTabActive(tab, false);
  await assertIfWindowGetSuspended(tab, { shouldBeSuspended: true });

  info(`remove tab`);
  await tab.close();
});

add_task(async function testInactiveTabEverStartPlayingWontBeSuspended() {
  info(`open tab1 and play media`);
  const tab1 = await createTab(PAGE_NON_AUTOPLAY, { needCheck: true });
  await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: false });
  await playMedia(tab1, VIDEO_ID);

  info(`tab with playing media won't be suspended when it becomes inactive`);
  setTabActive(tab1, false);
  await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: false });

  info(
    `even if media is paused, keep tab running so that it could listen to media keys to control media in the future`
  );
  await pauseMedia(tab1, VIDEO_ID);
  await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: false });

  info(`open tab2 and play media`);
  const tab2 = await createTab(PAGE_NON_AUTOPLAY, { needCheck: true });
  await assertIfWindowGetSuspended(tab2, { shouldBeSuspended: false });
  await playMedia(tab2, VIDEO_ID);

  info(
    `as inactive tab1 doesn't own main controller, it should be suspended again`
  );
  await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: true });

  info(`remove tabs`);
  await Promise.all([tab1.close(), tab2.close()]);
});

add_task(
  async function testInactiveTabWithRunningAudioContextWontBeSuspended() {
    info(`open tab and start an audio context (AC)`);
    const tab = await createTab("about:blank");
    await startAudioContext(tab);
    await assertIfWindowGetSuspended(tab, { shouldBeSuspended: false });

    info(`tab with running AC won't be suspended when it becomes inactive`);
    setTabActive(tab, false);
    await assertIfWindowGetSuspended(tab, { shouldBeSuspended: false });

    info(`if AC has been suspended, then inactive tab should be suspended`);
    await suspendAudioContext(tab);
    await assertIfWindowGetSuspended(tab, { shouldBeSuspended: true });

    info(`remove tab`);
    await tab.close();
  }
);

/**
 * The following are helper functions.
 */
async function createTab(url, needCheck = false) {
  const tab = await createLoadedTabWrapper(url, { needCheck });
  return tab;
}

function assertIfWindowGetSuspended(tab, { shouldBeSuspended }) {
  return SpecialPowers.spawn(
    tab.linkedBrowser,
    [shouldBeSuspended],
    expectedSuspend => {
      const isSuspended = content.windowUtils.suspendedByBrowsingContextGroup;
      is(
        expectedSuspend,
        isSuspended,
        `window suspended state (${isSuspended}) is equal to the expected`
      );
    }
  );
}

function setTabActive(tab, isActive) {
  tab.linkedBrowser.docShellIsActive = isActive;
}

function startAudioContext(tab) {
  return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
    content.ac = new content.AudioContext();
    await new Promise(r => (content.ac.onstatechange = r));
    ok(content.ac.state == "running", `Audio context started running`);
  });
}

function suspendAudioContext(tab) {
  return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
    await content.ac.suspend();
    ok(content.ac.state == "suspended", `Audio context is suspended`);
  });
}