summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/webidl-api/head_webidl_api.js
blob: 3e8e094a205f2f2180f24adc3e86ebd1bfdacdc6 (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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
/* import-globals-from ../head.js */

/* exported getBackgroundServiceWorkerRegistration, waitForTerminatedWorkers,
 *          runExtensionAPITest */

"use strict";

ChromeUtils.defineESModuleGetters(this, {
  ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs",
  TestUtils: "resource://testing-common/TestUtils.sys.mjs",
});

add_setup(function checkExtensionsWebIDLEnabled() {
  equal(
    AppConstants.MOZ_WEBEXT_WEBIDL_ENABLED,
    true,
    "WebExtensions WebIDL bindings build time flag should be enabled"
  );
});

function getBackgroundServiceWorkerRegistration(extension) {
  const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
    Ci.nsIServiceWorkerManager
  );

  const swRegs = swm.getAllRegistrations();
  const scope = `moz-extension://${extension.uuid}/`;

  for (let i = 0; i < swRegs.length; i++) {
    let regInfo = swRegs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
    if (regInfo.scope === scope) {
      return regInfo;
    }
  }
}

function waitForTerminatedWorkers(swRegInfo) {
  info(`Wait all ${swRegInfo.scope} workers to be terminated`);
  return TestUtils.waitForCondition(() => {
    const { evaluatingWorker, installingWorker, waitingWorker, activeWorker } =
      swRegInfo;
    return !(
      evaluatingWorker ||
      installingWorker ||
      waitingWorker ||
      activeWorker
    );
  }, `wait workers for scope ${swRegInfo.scope} to be terminated`);
}

function unmockHandleAPIRequest(extPage) {
  return extPage.spawn([], () => {
    const { ExtensionAPIRequestHandler } = ChromeUtils.importESModule(
      "resource://gre/modules/ExtensionProcessScript.sys.mjs"
    );

    // Unmock ExtensionAPIRequestHandler.
    if (ExtensionAPIRequestHandler._handleAPIRequest_orig) {
      ExtensionAPIRequestHandler.handleAPIRequest =
        ExtensionAPIRequestHandler._handleAPIRequest_orig;
      delete ExtensionAPIRequestHandler._handleAPIRequest_orig;
    }
  });
}

function mockHandleAPIRequest(extPage, mockHandleAPIRequest) {
  mockHandleAPIRequest =
    mockHandleAPIRequest ||
    ((policy, request) => {
      const ExtError = request.window?.Error || Error;
      return {
        type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
        value: new ExtError(
          "mockHandleAPIRequest not defined by this test case"
        ),
      };
    });

  return extPage.legacySpawn(
    [ExtensionTestCommon.serializeFunction(mockHandleAPIRequest)],
    mockFnText => {
      const { ExtensionAPIRequestHandler } = ChromeUtils.importESModule(
        "resource://gre/modules/ExtensionProcessScript.sys.mjs"
      );

      mockFnText = `(() => {
        return (${mockFnText});
      })();`;
      // eslint-disable-next-line no-eval
      const mockFn = eval(mockFnText);

      // Mock ExtensionAPIRequestHandler.
      if (!ExtensionAPIRequestHandler._handleAPIRequest_orig) {
        ExtensionAPIRequestHandler._handleAPIRequest_orig =
          ExtensionAPIRequestHandler.handleAPIRequest;
      }

      ExtensionAPIRequestHandler.handleAPIRequest = function (policy, request) {
        if (request.apiNamespace === "test") {
          return this._handleAPIRequest_orig(policy, request);
        }
        return mockFn.call(this, policy, request);
      };
    }
  );
}

/**
 * An helper function used to run unit test that are meant to test the
 * Extension API webidl bindings helpers shared by all the webextensions
 * API namespaces.
 *
 * @param {string} testDescription
 *        Brief description of the test.
 * @param {object} [options]
 * @param {Function} options.backgroundScript
 *        Test function running in the extension global. This function
 *        does receive a parameter of type object with the following
 *        properties:
 *        - testLog(message): log a message on the terminal
 *        - testAsserts:
 *          - isErrorInstance(err): throw if err is not an Error instance
 *          - isInstanceOf(value, globalContructorName): throws if value
 *            is not an instance of global[globalConstructorName]
 *          - equal(val, exp, msg): throw an error including msg if
 *            val is not strictly equal to exp.
 * @param {Function} options.assertResults
 *        Function to be provided to assert the result returned by
 *        `backgroundScript`, or assert the error if it did throw.
 *        This function does receive a parameter of type object with
 *        the following properties:
 *        - testResult: the result returned (and resolved if the return
 *          value was a promise) from the call to `backgroundScript`
 *        - testError: the error raised (or rejected if the return value
 *          value was a promise) from the call to `backgroundScript`
 *        - extension: the extension wrapper created by this helper.
 * @param {Function} options.mockAPIRequestHandler
 *        Function to be used to mock mozIExtensionAPIRequestHandler.handleAPIRequest
 *        for the purpose of the test.
 *        This function received the same parameter that are listed in the idl
 *        definition (mozIExtensionAPIRequestHandling.webidl).
 * @param {string} [options.extensionId]
 *        Optional extension id for the test extension.
 */
async function runExtensionAPITest(
  testDescription,
  {
    backgroundScript,
    assertResults,
    mockAPIRequestHandler,
    extensionId = "test-ext-api-request-forward@mochitest",
  }
) {
  // Wraps the `backgroundScript` function to be execute in the target
  // extension global (currently only in a background service worker,
  // in follow-ups the same function should also be execute in
  // other supported extension globals, e.g. an extension page and
  // a content script).
  //
  // The test wrapper does also provide to `backgroundScript` some
  // helpers to be used as part of the test, these tests are meant to
  // only cover internals shared by all webidl API bindings through a
  // mock API namespace only available in tests (and so none of the tests
  // written with this helpers should be using the browser.test API namespace).
  function backgroundScriptWrapper(testParams, testFn) {
    const testLog = msg => {
      // console messages emitted by workers are not visible in the test logs if not
      // explicitly collected, and so this testLog helper method does use dump for now
      // (this way the logs will be visibile as part of the test logs).
      dump(`"${testParams.extensionId}": ${msg}\n`);
    };

    const testAsserts = {
      isErrorInstance(err) {
        if (!(err instanceof Error)) {
          throw new Error("Unexpected error: not an instance of Error");
        }
        return true;
      },
      isInstanceOf(value, globalConstructorName) {
        if (!(value instanceof self[globalConstructorName])) {
          throw new Error(
            `Unexpected error: expected instance of ${globalConstructorName}`
          );
        }
        return true;
      },
      equal(val, exp, msg) {
        if (val !== exp) {
          throw new Error(
            `Unexpected error: expected ${exp} but got ${val}. ${msg}`
          );
        }
      },
    };

    testLog(`Evaluating - test case "${testParams.testDescription}"`);
    self.onmessage = async evt => {
      testLog(`Running test case "${testParams.testDescription}"`);

      let testError = null;
      let testResult;
      try {
        testResult = await testFn({ testLog, testAsserts });
      } catch (err) {
        testError = { message: err.message, stack: err.stack };
        testLog(`Unexpected test error: ${err} :: ${err.stack}\n`);
      }

      evt.ports[0].postMessage({ success: !testError, testError, testResult });

      testLog(`Test case "${testParams.testDescription}" executed`);
    };
    testLog(`Wait onmessage event - test case "${testParams.testDescription}"`);
  }

  async function assertTestResult(result) {
    if (assertResults) {
      await assertResults(result);
    } else {
      equal(result.testError, undefined, "Expect no errors");
      ok(result.success, "Test completed successfully");
    }
  }

  async function runTestCaseInWorker({ page, extension }) {
    info(`*** Run test case in an extension service worker`);
    const result = await page.legacySpawn([], async () => {
      const { active } = await content.navigator.serviceWorker.ready;
      const { port1, port2 } = new MessageChannel();

      return new Promise(resolve => {
        port1.onmessage = evt => resolve(evt.data);
        active.postMessage("run-test", [port2]);
      });
    });
    info(`*** Assert test case results got from extension service worker`);
    await assertTestResult({ ...result, extension });
  }

  // NOTE: prefixing this with `function ` is needed because backgroundScript
  // is an object property and so it is going to be stringified as
  // `backgroundScript() { ... }` (which would be detected as a syntax error
  // on the worker script evaluation phase).
  const scriptFnParam = ExtensionTestCommon.serializeFunction(backgroundScript);
  const testOptsParam = `${JSON.stringify({ testDescription, extensionId })}`;

  const testExtData = {
    useAddonManager: "temporary",
    manifest: {
      version: "1",
      background: {
        service_worker: "test-sw.js",
      },
      browser_specific_settings: {
        gecko: { id: extensionId },
      },
    },
    files: {
      "page.html": `<!DOCTYPE html>
        <head><meta charset="utf-8"></head>
        <body>
          <script src="test-sw.js"></script>
        </body>`,
      "test-sw.js": `
        (${backgroundScriptWrapper})(${testOptsParam}, ${scriptFnParam});
      `,
    },
  };

  let cleanupCalled = false;
  let extension;
  let page;
  let swReg;

  async function testCleanup() {
    if (cleanupCalled) {
      return;
    }

    cleanupCalled = true;
    await unmockHandleAPIRequest(page);
    await page.close();
    await extension.unload();
    await waitForTerminatedWorkers(swReg);
  }

  info(`Start test case "${testDescription}"`);
  extension = ExtensionTestUtils.loadExtension(testExtData);
  await extension.startup();

  swReg = getBackgroundServiceWorkerRegistration(extension);
  ok(swReg, "Extension background.service_worker should be registered");

  page = await ExtensionTestUtils.loadContentPage(
    `moz-extension://${extension.uuid}/page.html`,
    { extension }
  );

  registerCleanupFunction(testCleanup);

  await mockHandleAPIRequest(page, mockAPIRequestHandler);
  await runTestCaseInWorker({ page, extension });
  await testCleanup();
  info(`End test case "${testDescription}"`);
}