summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_eventpage_messaging.js
blob: b3ad24c461955a3c583f4f0537ee5facb4d1e9b4 (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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
"use strict";

// This test checks that the suspension of the event page is delayed when the
// runtime.onConnect / runtime.onMessage events are involved.
//
// Another test (test_ext_eventpage_messaging_wakeup.js) verifies that the event
// page wakes up when these events are to be triggered.

const { ExtensionTestCommon } = ChromeUtils.importESModule(
  "resource://testing-common/ExtensionTestCommon.sys.mjs"
);

add_setup(() => {
  // In this test, we want to verify that an idle timeout is reset when
  // extension messages are set. To avoid waiting for too long, reduce the
  // default timeout to a short value. To avoid premature test termination,
  // this timeout should be sufficiently large to run the relevant logic that
  // is supposed to postpone event page termination:
  // - The idle timer starts when the background has loaded.
  // - The idle timer should reset when expected by tests.
  Services.prefs.setIntPref("extensions.background.idle.timeout", 1000);
});

async function loadEventPageAndExtensionPage({
  backgroundScript,
  extensionPageScript,
}) {
  let extension = ExtensionTestUtils.loadExtension({
    // Delay startup, to ensure that the event page does not suspend until we
    // have started the extension page that runs extensionPageScript.
    startupReason: "APP_STARTUP",
    // APP_STARTUP is not enough, delayedStartup is needed (bug 1756225).
    delayedStartup: true,
    manifest: {
      background: { persistent: false },
    },
    background: backgroundScript,
    files: {
      "page.html": `<!DOCTYPE html><script src="page.js"></script>`,
      "page.js": extensionPageScript,
    },
  });

  // Delay event page startup until notifyEarlyStartup+notifyLateStartup below.
  await ExtensionTestCommon.resetStartupPromises();
  await extension.startup();

  // Start extension page first, so that it can register runtime.onSuspend
  // before there is any chance of encountering a suspended event page.
  let contentPage = await ExtensionTestUtils.loadContentPage(
    `moz-extension://${extension.uuid}/page.html`
  );

  info("Extension page loaded, permitting event page to start up");
  await ExtensionTestCommon.notifyEarlyStartup();
  await ExtensionTestCommon.notifyLateStartup();

  await extension.awaitMessage("FINAL_SUSPEND");
  info("Received FINAL_SUSPEND, awaiting full event page shutdown.");
  await promiseExtensionEvent(extension, "shutdown-background-script");
  await contentPage.close();
  await extension.unload();
}

add_task(async function test_runtime_onMessage_cancels_suspend() {
  await loadEventPageAndExtensionPage({
    backgroundScript() {
      // This background script registers listeners without calling any other
      // extension API. This ensures that if the event page suspend is canceled,
      // that it was intentionally done by the listener, and not as a side
      // effect of an unrelated extension API call.
      browser.runtime.onMessage.addListener(msg => {
        return Promise.resolve(`bg_pong:${msg}`);
      });
    },
    extensionPageScript() {
      let cancelCount = 0;
      browser.runtime.onSuspendCanceled.addListener(() => {
        browser.test.assertEq(1, ++cancelCount, "onSuspendCanceled 1x");
      });
      let suspendCount = 0;
      let firstSuspendTestCompleted = false;
      browser.runtime.onSuspend.addListener(async () => {
        // We expect 2x suspend: first one we cancel, second one is final.
        if (++suspendCount === 1) {
          // First suspend attempt.
          browser.test.assertEq(0, cancelCount, "Not suspended yet");
          let res = await browser.runtime.sendMessage("ping");
          browser.test.assertEq(1, cancelCount, "onMessage cancels suspend");
          browser.test.assertEq("bg_pong:ping", res, "onMessage result");
          firstSuspendTestCompleted = true;
        } else {
          browser.test.assertTrue(firstSuspendTestCompleted, "First test done");
          browser.test.assertEq(2, suspendCount, "Second onSuspend");
          browser.test.sendMessage("FINAL_SUSPEND");
        }
      });
      browser.test.log("Waiting for background to be suspended");
    },
  });
});

add_task(async function test_runtime_onConnect_cancels_suspend() {
  await loadEventPageAndExtensionPage({
    backgroundScript() {
      // This background script registers listeners without calling any other
      // extension API. This ensures that if the event page suspend is canceled,
      // that it was intentionally done by the listener, and not as a side
      // effect of an unrelated extension API call.
      browser.runtime.onConnect.addListener(() => {
        // Set by extensionPageScript before runtime.connect():
        globalThis.notify_extensionPage_got_onConnect();
      });
    },
    extensionPageScript() {
      let cancelCount = 0;
      browser.runtime.onSuspendCanceled.addListener(() => {
        browser.test.assertEq(1, ++cancelCount, "onSuspendCanceled 1x");
      });
      let suspendCount = 0;
      let firstSuspendTestCompleted = false;
      let port; // Prevent port from being gc'd during test.
      browser.runtime.onSuspend.addListener(async () => {
        // We expect 2x suspend: first one we cancel, second one is final.
        if (++suspendCount === 1) {
          // First suspend attempt.
          browser.test.assertEq(0, cancelCount, "Not suspended yet");
          // Call runtime.connect() twice:
          // 1. First connect() should be triggering the reset.
          // 2. We are immediately notified when runtime.onConnect is called.
          // 2. We call connect() again to have another page->parent->background
          //    roundtrip. This ensures that enough time to have been passed to
          //    allow the first runtime.onConnect handling to have finished,
          //    and to have triggeres onSuspendCanceled as desired.
          for (let i = 0; i < 2; ++i) {
            await new Promise(resolve => {
              let bgGlobal = browser.extension.getBackgroundPage();
              browser.test.assertTrue(!!bgGlobal, "Event page still running");
              bgGlobal.notify_extensionPage_got_onConnect = resolve;
              port = browser.runtime.connect({});
            });
          }
          browser.test.assertEq(1, cancelCount, "onConnect cancels suspend");
          firstSuspendTestCompleted = true;
        } else {
          browser.test.assertTrue(firstSuspendTestCompleted, "First test done");
          browser.test.assertEq(2, suspendCount, "Second onSuspend");
          browser.test.assertEq(null, port.error, "port has no error");
          browser.test.sendMessage("FINAL_SUSPEND");
        }
      });
      browser.test.log("Waiting for background to be suspended");
    },
  });
});

add_task(async function test_runtime_Port_onMessage_cancels_suspend() {
  await loadEventPageAndExtensionPage({
    backgroundScript() {
      // This background script registers listeners without calling any other
      // extension API. This ensures that if the event page suspend is canceled,
      // that it was intentionally done by the listener, and not as a side
      // effect of an unrelated extension API call.
      browser.runtime.onConnect.addListener(port => {
        port.onMessage.addListener(() => {
          // Set by extensionPageScript before runtime.connect():
          globalThis.notify_extensionPage_got_port_onMessage();
        });
      });
    },
    extensionPageScript() {
      let cancelCount = 0;
      browser.runtime.onSuspendCanceled.addListener(() => {
        browser.test.assertEq(1, ++cancelCount, "onSuspendCanceled 1x");
      });
      let suspendCount = 0;
      let firstSuspendTestCompleted = false;
      let port;
      browser.runtime.onSuspend.addListener(async () => {
        // We expect 2x suspend: first one we cancel, second one is final.
        if (++suspendCount === 1) {
          // First suspend attempt.
          browser.test.assertEq(0, cancelCount, "Not suspended yet");
          browser.test.assertTrue(!!port, "Should run after we opened a port");
          // Call port.postMessage() twice:
          // 1. First port.postMessage() should be triggering the reset.
          // 2. We are immediately notified when runtime.onMessage is called.
          // 2. We postMessage() again to have another page->parent->background
          //    roundtrip. This ensures that enough time to have been passed to
          //    allow the first port.onMessage handling to have finished,
          //    and to have triggeres onSuspendCanceled as desired.
          for (let i = 0; i < 2; ++i) {
            await new Promise(resolve => {
              let bgGlobal = browser.extension.getBackgroundPage();
              browser.test.assertTrue(!!bgGlobal, "Event page still running");
              bgGlobal.notify_extensionPage_got_port_onMessage = resolve;
              port.postMessage("");
            });
          }
          browser.test.assertEq(
            1,
            cancelCount,
            "port.onMessage cancels suspend"
          );
          firstSuspendTestCompleted = true;
        } else {
          browser.test.assertTrue(firstSuspendTestCompleted, "First test done");
          browser.test.assertEq(2, suspendCount, "Second onSuspend");
          browser.test.sendMessage("FINAL_SUSPEND");
        }
      });
      browser.runtime.getBackgroundPage(bgGlobal => {
        browser.test.assertTrue(!!bgGlobal, "Event page has started");
        // Since the event page has started, this should trigger onConnect in
        // the event page. If somehow the event page has suspended in the
        // meantime, then we will detect that in runtime.onSuspend (and fail).
        port = browser.runtime.connect({});
        // Assuming that runtime.onConnect in the event page has received the
        // port and started listening, we should now wait for an attempt to
        // suspend the event page (and try to cancel that via port.onMessage).
        browser.test.log("Waiting for background to be suspended");
      });
    },
  });
});