Rename Toolkit's webextensions component directory to better reflect what it is.

This commit is contained in:
NTD 2018-02-09 11:10:00 -05:00 committed by Roy Tam
commit d7d16e7fbb
242 changed files with 4 additions and 2 deletions

View file

@ -0,0 +1,9 @@
"use strict";
module.exports = { // eslint-disable-line no-undef
"extends": "../../../../../testing/xpcshell/xpcshell.eslintrc.js",
"globals": {
"browser": false,
},
};

View file

@ -0,0 +1,12 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div>Download HTML File</div>
</body>
</html>

View file

@ -0,0 +1 @@
This is a sample file used in download tests.

View file

@ -0,0 +1,111 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
/* exported createHttpServer, promiseConsoleOutput, cleanupDir */
Components.utils.import("resource://gre/modules/Task.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Timer.jsm");
Components.utils.import("resource://testing-common/AddonTestUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
"resource://gre/modules/ExtensionManagement.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils",
"resource://testing-common/ExtensionXPCShellUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
"resource://testing-common/httpd.js");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
ExtensionTestUtils.init(this);
/**
* Creates a new HttpServer for testing, and begins listening on the
* specified port. Automatically shuts down the server when the test
* unit ends.
*
* @param {integer} [port]
* The port to listen on. If omitted, listen on a random
* port. The latter is the preferred behavior.
*
* @returns {HttpServer}
*/
function createHttpServer(port = -1) {
let server = new HttpServer();
server.start(port);
do_register_cleanup(() => {
return new Promise(resolve => {
server.stop(resolve);
});
});
return server;
}
var promiseConsoleOutput = Task.async(function* (task) {
const DONE = `=== console listener ${Math.random()} done ===`;
let listener;
let messages = [];
let awaitListener = new Promise(resolve => {
listener = msg => {
if (msg == DONE) {
resolve();
} else {
void (msg instanceof Ci.nsIConsoleMessage);
messages.push(msg);
}
};
});
Services.console.registerListener(listener);
try {
let result = yield task();
Services.console.logStringMessage(DONE);
yield awaitListener;
return {messages, result};
} finally {
Services.console.unregisterListener(listener);
}
});
// Attempt to remove a directory. If the Windows OS is still using the
// file sometimes remove() will fail. So try repeatedly until we can
// remove it or we give up.
function cleanupDir(dir) {
let count = 0;
return new Promise((resolve, reject) => {
function tryToRemoveDir() {
count += 1;
try {
dir.remove(true);
} catch (e) {
// ignore
}
if (!dir.exists()) {
return resolve();
}
if (count >= 25) {
return reject(`Failed to cleanup directory: ${dir}`);
}
setTimeout(tryToRemoveDir, 100);
}
tryToRemoveDir();
});
}

View file

@ -0,0 +1,131 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
/* globals AppConstants, FileUtils */
/* exported getSubprocessCount, setupHosts, waitForSubprocessExit */
XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
"resource://testing-common/MockRegistry.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
let {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
// It's important that we use a space in this directory name to make sure we
// correctly handle executing batch files with spaces in their path.
let tmpDir = FileUtils.getDir("TmpD", ["Native Messaging"]);
tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
do_register_cleanup(() => {
tmpDir.remove(true);
});
function getPath(filename) {
return OS.Path.join(tmpDir.path, filename);
}
const ID = "native@tests.mozilla.org";
function* setupHosts(scripts) {
const PERMS = {unixMode: 0o755};
const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
const pythonPath = yield Subprocess.pathSearch(env.get("PYTHON"));
function* writeManifest(script, scriptPath, path) {
let body = `#!${pythonPath} -u\n${script.script}`;
yield OS.File.writeAtomic(scriptPath, body);
yield OS.File.setPermissions(scriptPath, PERMS);
let manifest = {
name: script.name,
description: script.description,
path,
type: "stdio",
allowed_extensions: [ID],
};
let manifestPath = getPath(`${script.name}.json`);
yield OS.File.writeAtomic(manifestPath, JSON.stringify(manifest));
return manifestPath;
}
switch (AppConstants.platform) {
case "macosx":
case "linux":
let dirProvider = {
getFile(property) {
if (property == "XREUserNativeMessaging") {
return tmpDir.clone();
} else if (property == "XRESysNativeMessaging") {
return tmpDir.clone();
}
return null;
},
};
Services.dirsvc.registerProvider(dirProvider);
do_register_cleanup(() => {
Services.dirsvc.unregisterProvider(dirProvider);
});
for (let script of scripts) {
let path = getPath(`${script.name}.py`);
yield writeManifest(script, path, path);
}
break;
case "win":
const REGKEY = String.raw`Software\Mozilla\NativeMessagingHosts`;
let registry = new MockRegistry();
do_register_cleanup(() => {
registry.shutdown();
});
for (let script of scripts) {
// It's important that we use a space in this filename. See directory
// name comment above.
let batPath = getPath(`batch ${script.name}.bat`);
let scriptPath = getPath(`${script.name}.py`);
let batBody = `@ECHO OFF\n${pythonPath} -u "${scriptPath}" %*\n`;
yield OS.File.writeAtomic(batPath, batBody);
// Create absolute and relative path versions of the entry.
for (let [name, path] of [[script.name, batPath],
[`relative.${script.name}`, OS.Path.basename(batPath)]]) {
script.name = name;
let manifestPath = yield writeManifest(script, scriptPath, path);
registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
`${REGKEY}\\${script.name}`, "", manifestPath);
}
}
break;
default:
ok(false, `Native messaging is not supported on ${AppConstants.platform}`);
}
}
function getSubprocessCount() {
return SubprocessImpl.Process.getWorker().call("getProcesses", [])
.then(result => result.size);
}
function waitForSubprocessExit() {
return SubprocessImpl.Process.getWorker().call("waitForNoProcesses", []).then(() => {
// Return to the main event loop to give IO handlers enough time to consume
// their remaining buffered input.
return new Promise(resolve => setTimeout(resolve, 0));
});
}

View file

@ -0,0 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* exported withSyncContext */
Components.utils.import("resource://gre/modules/Services.jsm", this);
Components.utils.import("resource://gre/modules/ExtensionCommon.jsm", this);
var {
BaseContext,
} = ExtensionCommon;
class Context extends BaseContext {
constructor(principal) {
super();
Object.defineProperty(this, "principal", {
value: principal,
configurable: true,
});
this.sandbox = Components.utils.Sandbox(principal, {wantXrays: false});
this.extension = {id: "test@web.extension"};
}
get cloneScope() {
return this.sandbox;
}
}
/**
* Call the given function with a newly-constructed context.
* Unload the context on the way out.
*
* @param {function} f the function to call
*/
function* withContext(f) {
const ssm = Services.scriptSecurityManager;
const PRINCIPAL1 = ssm.createCodebasePrincipalFromOrigin("http://www.example.org");
const context = new Context(PRINCIPAL1);
try {
yield* f(context);
} finally {
yield context.unload();
}
}
/**
* Like withContext(), but also turn on the "storage.sync" pref for
* the duration of the function.
* Calls to this function can be replaced with calls to withContext
* once the pref becomes on by default.
*
* @param {function} f the function to call
*/
function* withSyncContext(f) {
const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled";
let prefs = Services.prefs;
try {
prefs.setBoolPref(STORAGE_SYNC_PREF, true);
yield* withContext(f);
} finally {
prefs.clearUserPref(STORAGE_SYNC_PREF);
}
}

View file

@ -0,0 +1,13 @@
[DEFAULT]
head = head.js head_native_messaging.js
tail =
firefox-appdir = browser
skip-if = appname == "thunderbird" || os == "android"
subprocess = true
support-files =
data/**
tags = webextensions
[test_ext_native_messaging.js]
[test_ext_native_messaging_perf.js]
[test_ext_native_messaging_unresponsive.js]

View file

@ -0,0 +1,38 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://gre/modules/Preferences.jsm");
const ADDON_ID = "test@web.extension";
const aps = Cc["@mozilla.org/addons/policy-service;1"]
.getService(Ci.nsIAddonPolicyService).wrappedJSObject;
do_register_cleanup(() => {
aps.setAddonCSP(ADDON_ID, null);
});
add_task(function* test_addon_csp() {
equal(aps.baseCSP, Preferences.get("extensions.webextensions.base-content-security-policy"),
"Expected base CSP value");
equal(aps.defaultCSP, Preferences.get("extensions.webextensions.default-content-security-policy"),
"Expected default CSP value");
equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP,
"CSP for unknown add-on ID should be the default CSP");
const CUSTOM_POLICY = "script-src: 'self' https://xpcshell.test.custom.csp; object-src: 'none'";
aps.setAddonCSP(ADDON_ID, CUSTOM_POLICY);
equal(aps.getAddonCSP(ADDON_ID), CUSTOM_POLICY, "CSP should point to add-on's custom policy");
aps.setAddonCSP(ADDON_ID, null);
equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP,
"CSP should revert to default when set to null");
});

View file

@ -0,0 +1,85 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const cps = Cc["@mozilla.org/addons/content-policy;1"].getService(Ci.nsIAddonContentPolicy);
add_task(function* test_csp_validator() {
let checkPolicy = (policy, expectedResult, message = null) => {
do_print(`Checking policy: ${policy}`);
let result = cps.validateAddonCSP(policy);
equal(result, expectedResult);
};
checkPolicy("script-src 'self'; object-src 'self';",
null);
let hash = "'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='";
checkPolicy(`script-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash} 'unsafe-eval'; ` +
`object-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash}`,
null);
checkPolicy("",
"Policy is missing a required \u2018script-src\u2019 directive");
checkPolicy("object-src 'none';",
"Policy is missing a required \u2018script-src\u2019 directive");
checkPolicy("default-src 'self'", null,
"A valid default-src should count as a valid script-src or object-src");
checkPolicy("default-src 'self'; script-src 'self'", null,
"A valid default-src should count as a valid script-src or object-src");
checkPolicy("default-src 'self'; object-src 'self'", null,
"A valid default-src should count as a valid script-src or object-src");
checkPolicy("default-src 'self'; script-src http://example.com",
"\u2018script-src\u2019 directive contains a forbidden http: protocol source",
"A valid default-src should not allow an invalid script-src directive");
checkPolicy("default-src 'self'; object-src http://example.com",
"\u2018object-src\u2019 directive contains a forbidden http: protocol source",
"A valid default-src should not allow an invalid object-src directive");
checkPolicy("script-src 'self';",
"Policy is missing a required \u2018object-src\u2019 directive");
checkPolicy("script-src 'none'; object-src 'none'",
"\u2018script-src\u2019 must include the source 'self'");
checkPolicy("script-src 'self'; object-src 'none';",
null);
checkPolicy("script-src 'self' 'unsafe-inline'; object-src 'self';",
"\u2018script-src\u2019 directive contains a forbidden 'unsafe-inline' keyword");
let directives = ["script-src", "object-src"];
for (let [directive, other] of [directives, directives.slice().reverse()]) {
for (let src of ["https://*", "https://*.blogspot.com", "https://*"]) {
checkPolicy(`${directive} 'self' ${src}; ${other} 'self';`,
`https: wildcard sources in \u2018${directive}\u2019 directives must include at least one non-generic sub-domain (e.g., *.example.com rather than *.com)`);
}
checkPolicy(`${directive} 'self' https:; ${other} 'self';`,
`https: protocol requires a host in \u2018${directive}\u2019 directives`);
checkPolicy(`${directive} 'self' http://example.com; ${other} 'self';`,
`\u2018${directive}\u2019 directive contains a forbidden http: protocol source`);
for (let protocol of ["http", "ftp", "meh"]) {
checkPolicy(`${directive} 'self' ${protocol}:; ${other} 'self';`,
`\u2018${directive}\u2019 directive contains a forbidden ${protocol}: protocol source`);
}
checkPolicy(`${directive} 'self' 'nonce-01234'; ${other} 'self';`,
`\u2018${directive}\u2019 directive contains a forbidden 'nonce-*' keyword`);
}
});

View file

@ -0,0 +1,210 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_alarm_without_permissions() {
function backgroundScript() {
browser.test.assertTrue(!browser.alarms,
"alarm API is not available when the alarm permission is not required");
browser.test.notifyPass("alarms_permission");
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: [],
},
});
yield extension.startup();
yield extension.awaitFinish("alarms_permission");
yield extension.unload();
});
add_task(function* test_alarm_fires() {
function backgroundScript() {
let ALARM_NAME = "test_ext_alarms";
let timer;
browser.alarms.onAlarm.addListener(alarm => {
browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the correct name");
clearTimeout(timer);
browser.test.notifyPass("alarm-fires");
});
browser.alarms.create(ALARM_NAME, {delayInMinutes: 0.02});
timer = setTimeout(async () => {
browser.test.fail("alarm fired within expected time");
let wasCleared = await browser.alarms.clear(ALARM_NAME);
browser.test.assertTrue(wasCleared, "alarm was cleared");
browser.test.notifyFail("alarm-fires");
}, 10000);
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield extension.startup();
yield extension.awaitFinish("alarm-fires");
yield extension.unload();
});
add_task(function* test_alarm_fires_with_when() {
function backgroundScript() {
let ALARM_NAME = "test_ext_alarms";
let timer;
browser.alarms.onAlarm.addListener(alarm => {
browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the expected name");
clearTimeout(timer);
browser.test.notifyPass("alarm-when");
});
browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000});
timer = setTimeout(async () => {
browser.test.fail("alarm fired within expected time");
let wasCleared = await browser.alarms.clear(ALARM_NAME);
browser.test.assertTrue(wasCleared, "alarm was cleared");
browser.test.notifyFail("alarm-when");
}, 10000);
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield extension.startup();
yield extension.awaitFinish("alarm-when");
yield extension.unload();
});
add_task(function* test_alarm_clear_non_matching_name() {
async function backgroundScript() {
let ALARM_NAME = "test_ext_alarms";
browser.alarms.create(ALARM_NAME, {when: Date.now() + 2000});
let wasCleared = await browser.alarms.clear(ALARM_NAME + "1");
browser.test.assertFalse(wasCleared, "alarm was not cleared");
let alarms = await browser.alarms.getAll();
browser.test.assertEq(1, alarms.length, "alarm was not removed");
browser.test.notifyPass("alarm-clear");
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield extension.startup();
yield extension.awaitFinish("alarm-clear");
yield extension.unload();
});
add_task(function* test_alarm_get_and_clear_single_argument() {
async function backgroundScript() {
browser.alarms.create({when: Date.now() + 2000});
let alarm = await browser.alarms.get();
browser.test.assertEq("", alarm.name, "expected alarm returned");
let wasCleared = await browser.alarms.clear();
browser.test.assertTrue(wasCleared, "alarm was cleared");
let alarms = await browser.alarms.getAll();
browser.test.assertEq(0, alarms.length, "alarm was removed");
browser.test.notifyPass("alarm-single-arg");
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield extension.startup();
yield extension.awaitFinish("alarm-single-arg");
yield extension.unload();
});
add_task(function* test_get_get_all_clear_all_alarms() {
async function backgroundScript() {
const ALARM_NAME = "test_alarm";
let suffixes = [0, 1, 2];
for (let suffix of suffixes) {
browser.alarms.create(ALARM_NAME + suffix, {when: Date.now() + (suffix + 1) * 10000});
}
let alarms = await browser.alarms.getAll();
browser.test.assertEq(suffixes.length, alarms.length, "expected number of alarms were found");
alarms.forEach((alarm, index) => {
browser.test.assertEq(ALARM_NAME + index, alarm.name, "alarm has the expected name");
});
for (let suffix of suffixes) {
let alarm = await browser.alarms.get(ALARM_NAME + suffix);
browser.test.assertEq(ALARM_NAME + suffix, alarm.name, "alarm has the expected name");
browser.test.sendMessage(`get-${suffix}`);
}
let wasCleared = await browser.alarms.clear(ALARM_NAME + suffixes[0]);
browser.test.assertTrue(wasCleared, "alarm was cleared");
alarms = await browser.alarms.getAll();
browser.test.assertEq(2, alarms.length, "alarm was removed");
let alarm = await browser.alarms.get(ALARM_NAME + suffixes[0]);
browser.test.assertEq(undefined, alarm, "non-existent alarm is undefined");
browser.test.sendMessage(`get-invalid`);
wasCleared = await browser.alarms.clearAll();
browser.test.assertTrue(wasCleared, "alarms were cleared");
alarms = await browser.alarms.getAll();
browser.test.assertEq(0, alarms.length, "no alarms exist");
browser.test.sendMessage("clearAll");
browser.test.sendMessage("clear");
browser.test.sendMessage("getAll");
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield Promise.all([
extension.startup(),
extension.awaitMessage("getAll"),
extension.awaitMessage("get-0"),
extension.awaitMessage("get-1"),
extension.awaitMessage("get-2"),
extension.awaitMessage("clear"),
extension.awaitMessage("get-invalid"),
extension.awaitMessage("clearAll"),
]);
yield extension.unload();
});

View file

@ -0,0 +1,33 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_cleared_alarm_does_not_fire() {
async function backgroundScript() {
let ALARM_NAME = "test_ext_alarms";
browser.alarms.onAlarm.addListener(alarm => {
browser.test.fail("cleared alarm does not fire");
browser.test.notifyFail("alarm-cleared");
});
browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000});
let wasCleared = await browser.alarms.clear(ALARM_NAME);
browser.test.assertTrue(wasCleared, "alarm was cleared");
await new Promise(resolve => setTimeout(resolve, 2000));
browser.test.notifyPass("alarm-cleared");
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield extension.startup();
yield extension.awaitFinish("alarm-cleared");
yield extension.unload();
});

View file

@ -0,0 +1,44 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_periodic_alarm_fires() {
function backgroundScript() {
const ALARM_NAME = "test_ext_alarms";
let count = 0;
let timer;
browser.alarms.onAlarm.addListener(async alarm => {
browser.test.assertEq(alarm.name, ALARM_NAME, "alarm has the expected name");
if (count++ === 3) {
clearTimeout(timer);
let wasCleared = await browser.alarms.clear(ALARM_NAME);
browser.test.assertTrue(wasCleared, "alarm was cleared");
browser.test.notifyPass("alarm-periodic");
}
});
browser.alarms.create(ALARM_NAME, {periodInMinutes: 0.02});
timer = setTimeout(async () => {
browser.test.fail("alarm fired expected number of times");
let wasCleared = await browser.alarms.clear(ALARM_NAME);
browser.test.assertTrue(wasCleared, "alarm was cleared");
browser.test.notifyFail("alarm-periodic");
}, 30000);
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield extension.startup();
yield extension.awaitFinish("alarm-periodic");
yield extension.unload();
});

View file

@ -0,0 +1,44 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_duplicate_alarm_name_replaces_alarm() {
function backgroundScript() {
let count = 0;
browser.alarms.onAlarm.addListener(async alarm => {
if (alarm.name === "master alarm") {
browser.alarms.create("child alarm", {delayInMinutes: 0.05});
let results = await browser.alarms.getAll();
browser.test.assertEq(2, results.length, "exactly two alarms exist");
browser.test.assertEq("master alarm", results[0].name, "first alarm has the expected name");
browser.test.assertEq("child alarm", results[1].name, "second alarm has the expected name");
if (count++ === 3) {
await browser.alarms.clear("master alarm");
await browser.alarms.clear("child alarm");
browser.test.notifyPass("alarm-duplicate");
}
} else {
browser.test.fail("duplicate named alarm replaced existing alarm");
browser.test.notifyFail("alarm-duplicate");
}
});
browser.alarms.create("master alarm", {delayInMinutes: 0.025, periodInMinutes: 0.025});
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["alarms"],
},
});
yield extension.startup();
yield extension.awaitFinish("alarm-duplicate");
yield extension.unload();
});

View file

@ -0,0 +1,64 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
let {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
function getNextContext() {
return new Promise(resolve => {
Management.on("proxy-context-load", function listener(type, context) {
Management.off("proxy-context-load", listener);
resolve(context);
});
});
}
add_task(function* test_storage_api_without_permissions() {
let extension = ExtensionTestUtils.loadExtension({
background() {
// Force API initialization.
void browser.storage;
},
manifest: {
permissions: [],
},
});
let contextPromise = getNextContext();
yield extension.startup();
let context = yield contextPromise;
// Force API initialization.
void context.apiObj;
ok(!("storage" in context.apiObj),
"The storage API should not be initialized");
yield extension.unload();
});
add_task(function* test_storage_api_with_permissions() {
let extension = ExtensionTestUtils.loadExtension({
background() {
void browser.storage;
},
manifest: {
permissions: ["storage"],
},
});
let contextPromise = getNextContext();
yield extension.startup();
let context = yield contextPromise;
// Force API initialization.
void context.apiObj;
equal(typeof context.apiObj.storage, "object",
"The storage API should be initialized");
yield extension.unload();
});

View file

@ -0,0 +1,91 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
const {
SchemaAPIManager,
} = ExtensionCommon;
this.unknownvar = "Some module-global var";
var gUniqueId = 0;
// SchemaAPIManager's loadScript uses loadSubScript to load a script. This
// requires a local (resource://) URL. So create such a temporary URL for
// testing.
function toLocalURI(code) {
let dataUrl = `data:charset=utf-8,${encodeURIComponent(code)}`;
let uniqueResPart = `need-a-local-uri-for-subscript-loading-${++gUniqueId}`;
Services.io.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler)
.setSubstitution(uniqueResPart, Services.io.newURI(dataUrl, null, null));
return `resource://${uniqueResPart}`;
}
add_task(function* test_global_isolation() {
let manA = new SchemaAPIManager("procA");
let manB = new SchemaAPIManager("procB");
// The "global" variable should be persistent and shared.
manA.loadScript(toLocalURI`global.globalVar = 1;`);
do_check_eq(manA.global.globalVar, 1);
do_check_eq(manA.global.unknownvar, undefined);
manA.loadScript(toLocalURI`global.canSeeGlobal = global.globalVar;`);
do_check_eq(manA.global.canSeeGlobal, 1);
// Each loadScript call should have their own scope, and global is shared.
manA.loadScript(toLocalURI`this.aVar = 1; global.thisScopeVar = aVar`);
do_check_eq(manA.global.aVar, undefined);
do_check_eq(manA.global.thisScopeVar, 1);
manA.loadScript(toLocalURI`global.differentScopeVar = this.aVar;`);
do_check_eq(manA.global.differentScopeVar, undefined);
manA.loadScript(toLocalURI`global.cantSeeOtherScope = typeof aVar;`);
do_check_eq(manA.global.cantSeeOtherScope, "undefined");
manB.loadScript(toLocalURI`global.tryReadOtherGlobal = global.tryagain;`);
do_check_eq(manA.global.tryReadOtherGlobal, undefined);
// Cu.import without second argument exports to the caller's global. Let's
// verify that it does not leak to the SchemaAPIManager's global.
do_check_eq(typeof ExtensionUtils, "undefined"); // Sanity check #1.
manA.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
do_check_eq(manA.global.hasExtUtils, "undefined"); // Sanity check #2
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
do_check_eq(typeof ExtensionUtils, "object"); // Sanity check #3.
manA.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
do_check_eq(manA.global.hasExtUtils, "undefined");
manB.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
do_check_eq(manB.global.hasExtUtils, "undefined");
// Confirm that Cu.import does not leak between SchemaAPIManager globals.
manA.loadScript(toLocalURI`
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
global.hasExtUtils = typeof ExtensionUtils;
`);
do_check_eq(manA.global.hasExtUtils, "object"); // Sanity check.
manB.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
do_check_eq(manB.global.hasExtUtils, "undefined");
// Prototype modifications should be isolated.
manA.loadScript(toLocalURI`
Object.prototype.modifiedByA = "Prrft";
global.fromA = {};
`);
manA.loadScript(toLocalURI`
global.fromAagain = {};
`);
manB.loadScript(toLocalURI`
global.fromB = {};
`);
do_check_eq(manA.global.modifiedByA, "Prrft");
do_check_eq(manA.global.fromA.modifiedByA, "Prrft");
do_check_eq(manA.global.fromAagain.modifiedByA, "Prrft");
do_check_eq(manB.global.modifiedByA, undefined);
do_check_eq(manB.global.fromB.modifiedByA, undefined);
});

View file

@ -0,0 +1,23 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
/* eslint-disable mozilla/balanced-listeners */
add_task(function* test_DOMContentLoaded_in_generated_background_page() {
let extension = ExtensionTestUtils.loadExtension({
background() {
function reportListener(event) {
browser.test.sendMessage("eventname", event.type);
}
document.addEventListener("DOMContentLoaded", reportListener);
window.addEventListener("load", reportListener);
},
});
yield extension.startup();
equal("DOMContentLoaded", yield extension.awaitMessage("eventname"));
equal("load", yield extension.awaitMessage("eventname"));
yield extension.unload();
});

View file

@ -0,0 +1,24 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_reload_generated_background_page() {
let extension = ExtensionTestUtils.loadExtension({
background() {
if (location.hash !== "#firstrun") {
browser.test.sendMessage("first run");
location.hash = "#firstrun";
browser.test.assertEq("#firstrun", location.hash);
location.reload();
} else {
browser.test.notifyPass("second run");
}
},
});
yield extension.startup();
yield extension.awaitMessage("first run");
yield extension.awaitFinish("second run");
yield extension.unload();
});

View file

@ -0,0 +1,22 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://testing-common/PlacesTestUtils.jsm");
add_task(function* test_global_history() {
let extension = ExtensionTestUtils.loadExtension({
background() {
browser.test.sendMessage("background-loaded", location.href);
},
});
yield extension.startup();
let backgroundURL = yield extension.awaitMessage("background-loaded");
yield extension.unload();
let exists = yield PlacesTestUtils.isPageInDB(backgroundURL);
ok(!exists, "Background URL should not be in history database");
});

View file

@ -0,0 +1,40 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://gre/modules/Preferences.jsm");
function* testBackgroundPage(expected) {
let extension = ExtensionTestUtils.loadExtension({
async background() {
browser.test.assertEq(window, browser.extension.getBackgroundPage(),
"Caller should be able to access itself as a background page");
browser.test.assertEq(window, await browser.runtime.getBackgroundPage(),
"Caller should be able to access itself as a background page");
browser.test.sendMessage("incognito", browser.extension.inIncognitoContext);
},
});
yield extension.startup();
let incognito = yield extension.awaitMessage("incognito");
equal(incognito, expected.incognito, "Expected incognito value");
yield extension.unload();
}
add_task(function* test_background_incognito() {
do_print("Test background page incognito value with permanent private browsing disabled");
yield testBackgroundPage({incognito: false});
do_print("Test background page incognito value with permanent private browsing enabled");
Preferences.set("browser.privatebrowsing.autostart", true);
do_register_cleanup(() => {
Preferences.reset("browser.privatebrowsing.autostart");
});
yield testBackgroundPage({incognito: true});
});

View file

@ -0,0 +1,72 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
function backgroundScript() {
let received_ports_number = 0;
const expected_received_ports_number = 1;
function countReceivedPorts(port) {
received_ports_number++;
if (port.name == "check-results") {
browser.runtime.onConnect.removeListener(countReceivedPorts);
browser.test.assertEq(expected_received_ports_number, received_ports_number, "invalid connect should not create a port");
browser.test.notifyPass("runtime.connect invalid params");
}
}
browser.runtime.onConnect.addListener(countReceivedPorts);
let childFrame = document.createElement("iframe");
childFrame.src = "extensionpage.html";
document.body.appendChild(childFrame);
}
function senderScript() {
let detected_invalid_connect_params = 0;
const invalid_connect_params = [
// too many params
["fake-extensions-id", {name: "fake-conn-name"}, "unexpected third params"],
// invalid params format
[{}, {}],
["fake-extensions-id", "invalid-connect-info-format"],
];
const expected_detected_invalid_connect_params = invalid_connect_params.length;
function assertInvalidConnectParamsException(params) {
try {
browser.runtime.connect(...params);
} catch (e) {
detected_invalid_connect_params++;
browser.test.assertTrue(e.toString().indexOf("Incorrect argument types for runtime.connect.") >= 0, "exception message is correct");
}
}
for (let params of invalid_connect_params) {
assertInvalidConnectParamsException(params);
}
browser.test.assertEq(expected_detected_invalid_connect_params, detected_invalid_connect_params, "all invalid runtime.connect params detected");
browser.runtime.connect(browser.runtime.id, {name: "check-results"});
}
let extensionData = {
background: backgroundScript,
files: {
"senderScript.js": senderScript,
"extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`,
},
};
add_task(function* test_backgroundRuntimeConnectParams() {
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitFinish("runtime.connect invalid params");
yield extension.unload();
});

View file

@ -0,0 +1,45 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* testBackgroundWindow() {
let extension = ExtensionTestUtils.loadExtension({
background() {
browser.test.log("background script executed");
browser.test.sendMessage("background-script-load");
let img = document.createElement("img");
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
document.body.appendChild(img);
img.onload = () => {
browser.test.log("image loaded");
let iframe = document.createElement("iframe");
iframe.src = "about:blank?1";
iframe.onload = () => {
browser.test.log("iframe loaded");
setTimeout(() => {
browser.test.notifyPass("background sub-window test done");
}, 0);
};
document.body.appendChild(iframe);
};
},
});
let loadCount = 0;
extension.onMessage("background-script-load", () => {
loadCount++;
});
yield extension.startup();
yield extension.awaitFinish("background sub-window test done");
equal(loadCount, 1, "background script loaded only once");
yield extension.unload();
});

View file

@ -0,0 +1,34 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* testBackgroundWindowProperties() {
let extension = ExtensionTestUtils.loadExtension({
background() {
let expectedValues = {
screenX: 0,
screenY: 0,
outerWidth: 0,
outerHeight: 0,
};
for (let k in window) {
try {
if (k in expectedValues) {
browser.test.assertEq(expectedValues[k], window[k],
`should return the expected value for window property: ${k}`);
} else {
void window[k];
}
} catch (e) {
browser.test.assertEq(null, e, `unexpected exception accessing window property: ${k}`);
}
}
browser.test.notifyPass("background.testWindowProperties.done");
},
});
yield extension.startup();
yield extension.awaitFinish("background.testWindowProperties.done");
yield extension.unload();
});

View file

@ -0,0 +1,190 @@
"use strict";
const global = this;
Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
BaseContext,
} = ExtensionCommon;
var {
EventManager,
SingletonEventManager,
} = ExtensionUtils;
class StubContext extends BaseContext {
constructor() {
let fakeExtension = {id: "test@web.extension"};
super("testEnv", fakeExtension);
this.sandbox = Cu.Sandbox(global);
}
get cloneScope() {
return this.sandbox;
}
}
add_task(function* test_post_unload_promises() {
let context = new StubContext();
let fail = result => {
ok(false, `Unexpected callback: ${result}`);
};
// Make sure promises resolve normally prior to unload.
let promises = [
context.wrapPromise(Promise.resolve()),
context.wrapPromise(Promise.reject({message: ""})).catch(() => {}),
];
yield Promise.all(promises);
// Make sure promises that resolve after unload do not trigger
// resolution handlers.
context.wrapPromise(Promise.resolve("resolved"))
.then(fail);
context.wrapPromise(Promise.reject({message: "rejected"}))
.then(fail, fail);
context.unload();
// The `setTimeout` ensures that we return to the event loop after
// promise resolution, which means we're guaranteed to return after
// any micro-tasks that get enqueued by the resolution handlers above.
yield new Promise(resolve => setTimeout(resolve, 0));
});
add_task(function* test_post_unload_listeners() {
let context = new StubContext();
let fireEvent;
let onEvent = new EventManager(context, "onEvent", fire => {
fireEvent = fire;
return () => {};
});
let fireSingleton;
let onSingleton = new SingletonEventManager(context, "onSingleton", callback => {
fireSingleton = () => {
Promise.resolve().then(callback);
};
return () => {};
});
let fail = event => {
ok(false, `Unexpected event: ${event}`);
};
// Check that event listeners aren't called after they've been removed.
onEvent.addListener(fail);
onSingleton.addListener(fail);
let promises = [
new Promise(resolve => onEvent.addListener(resolve)),
new Promise(resolve => onSingleton.addListener(resolve)),
];
fireEvent("onEvent");
fireSingleton("onSingleton");
// Both `fireEvent` calls are dispatched asynchronously, so they won't
// have fired by this point. The `fail` listeners that we remove now
// should not be called, even though the events have already been
// enqueued.
onEvent.removeListener(fail);
onSingleton.removeListener(fail);
// Wait for the remaining listeners to be called, which should always
// happen after the `fail` listeners would normally be called.
yield Promise.all(promises);
// Check that event listeners aren't called after the context has
// unloaded.
onEvent.addListener(fail);
onSingleton.addListener(fail);
// The EventManager `fire` callback always dispatches events
// asynchronously, so we need to test that any pending event callbacks
// aren't fired after the context unloads. We also need to test that
// any `fire` calls that happen *after* the context is unloaded also
// do not trigger callbacks.
fireEvent("onEvent");
Promise.resolve("onEvent").then(fireEvent);
fireSingleton("onSingleton");
Promise.resolve("onSingleton").then(fireSingleton);
context.unload();
// The `setTimeout` ensures that we return to the event loop after
// promise resolution, which means we're guaranteed to return after
// any micro-tasks that get enqueued by the resolution handlers above.
yield new Promise(resolve => setTimeout(resolve, 0));
});
class Context extends BaseContext {
constructor(principal) {
let fakeExtension = {id: "test@web.extension"};
super("testEnv", fakeExtension);
Object.defineProperty(this, "principal", {
value: principal,
configurable: true,
});
this.sandbox = Cu.Sandbox(principal, {wantXrays: false});
}
get cloneScope() {
return this.sandbox;
}
}
let ssm = Services.scriptSecurityManager;
const PRINCIPAL1 = ssm.createCodebasePrincipalFromOrigin("http://www.example.org");
const PRINCIPAL2 = ssm.createCodebasePrincipalFromOrigin("http://www.somethingelse.org");
// Test that toJSON() works in the json sandbox
add_task(function* test_stringify_toJSON() {
let context = new Context(PRINCIPAL1);
let obj = Cu.evalInSandbox("({hidden: true, toJSON() { return {visible: true}; } })", context.sandbox);
let stringified = context.jsonStringify(obj);
let expected = JSON.stringify({visible: true});
equal(stringified, expected, "Stringified object with toJSON() method is as expected");
});
// Test that stringifying in inaccessible property throws
add_task(function* test_stringify_inaccessible() {
let context = new Context(PRINCIPAL1);
let sandbox = context.sandbox;
let sandbox2 = Cu.Sandbox(PRINCIPAL2);
Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2);
let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox);
Assert.throws(() => {
context.jsonStringify(obj);
});
});
add_task(function* test_stringify_accessible() {
// Test that an accessible property from another global is included
let principal = Cu.getObjectPrincipal(Cu.Sandbox([PRINCIPAL1, PRINCIPAL2]));
let context = new Context(principal);
let sandbox = context.sandbox;
let sandbox2 = Cu.Sandbox(PRINCIPAL2);
Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2);
let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox);
let stringified = context.jsonStringify(obj);
let expected = JSON.stringify({local: true, nested: {subobject: true}});
equal(stringified, expected, "Stringified object with accessible property is as expected");
});

View file

@ -0,0 +1,76 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_downloads_api_namespace_and_permissions() {
function backgroundScript() {
browser.test.assertTrue(!!browser.downloads, "`downloads` API is present.");
browser.test.assertTrue(!!browser.downloads.FilenameConflictAction,
"`downloads.FilenameConflictAction` enum is present.");
browser.test.assertTrue(!!browser.downloads.InterruptReason,
"`downloads.InterruptReason` enum is present.");
browser.test.assertTrue(!!browser.downloads.DangerType,
"`downloads.DangerType` enum is present.");
browser.test.assertTrue(!!browser.downloads.State,
"`downloads.State` enum is present.");
browser.test.notifyPass("downloads tests");
}
let extensionData = {
background: backgroundScript,
manifest: {
permissions: ["downloads", "downloads.open", "downloads.shelf"],
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitFinish("downloads tests");
yield extension.unload();
});
add_task(function* test_downloads_open_permission() {
function backgroundScript() {
browser.test.assertFalse("open" in browser.downloads,
"`downloads.open` permission is required.");
browser.test.notifyPass("downloads tests");
}
let extensionData = {
background: backgroundScript,
manifest: {
permissions: ["downloads"],
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitFinish("downloads tests");
yield extension.unload();
});
add_task(function* test_downloads_open() {
async function backgroundScript() {
await browser.test.assertRejects(
browser.downloads.open(10),
"Invalid download id 10",
"The error is informative.");
browser.test.notifyPass("downloads tests");
// TODO: Once downloads.{pause,cancel,resume} lands (bug 1245602) test that this gives a good
// error when called with an incompleted download.
}
let extensionData = {
background: backgroundScript,
manifest: {
permissions: ["downloads", "downloads.open"],
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitFinish("downloads tests");
yield extension.unload();
});

View file

@ -0,0 +1,354 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
/* global OS */
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Downloads.jsm");
const gServer = createHttpServer();
gServer.registerDirectory("/data/", do_get_file("data"));
const WINDOWS = AppConstants.platform == "win";
const BASE = `http://localhost:${gServer.identity.primaryPort}/data`;
const FILE_NAME = "file_download.txt";
const FILE_URL = BASE + "/" + FILE_NAME;
const FILE_NAME_UNIQUE = "file_download(1).txt";
const FILE_LEN = 46;
let downloadDir;
function setup() {
downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
downloadDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
do_print(`Using download directory ${downloadDir.path}`);
Services.prefs.setIntPref("browser.download.folderList", 2);
Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, downloadDir);
do_register_cleanup(() => {
Services.prefs.clearUserPref("browser.download.folderList");
Services.prefs.clearUserPref("browser.download.dir");
let entries = downloadDir.directoryEntries;
while (entries.hasMoreElements()) {
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
ok(false, `Leftover file ${entry.path} in download directory`);
entry.remove(false);
}
downloadDir.remove(false);
});
}
function backgroundScript() {
let blobUrl;
browser.test.onMessage.addListener(async (msg, ...args) => {
if (msg == "download.request") {
let options = args[0];
if (options.blobme) {
let blob = new Blob(options.blobme);
delete options.blobme;
blobUrl = options.url = window.URL.createObjectURL(blob);
}
try {
let id = await browser.downloads.download(options);
browser.test.sendMessage("download.done", {status: "success", id});
} catch (error) {
browser.test.sendMessage("download.done", {status: "error", errmsg: error.message});
}
} else if (msg == "killTheBlob") {
window.URL.revokeObjectURL(blobUrl);
blobUrl = null;
}
});
browser.test.sendMessage("ready");
}
// This function is a bit of a sledgehammer, it looks at every download
// the browser knows about and waits for all active downloads to complete.
// But we only start one at a time and only do a handful in total, so
// this lets us test download() without depending on anything else.
async function waitForDownloads() {
let list = await Downloads.getList(Downloads.ALL);
let downloads = await list.getAll();
let inprogress = downloads.filter(dl => !dl.stopped);
return Promise.all(inprogress.map(dl => dl.whenSucceeded()));
}
// Create a file in the downloads directory.
function touch(filename) {
let file = downloadDir.clone();
file.append(filename);
file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
}
// Remove a file in the downloads directory.
function remove(filename, recursive = false) {
let file = downloadDir.clone();
file.append(filename);
file.remove(recursive);
}
add_task(function* test_downloads() {
setup();
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["downloads"],
},
});
function download(options) {
extension.sendMessage("download.request", options);
return extension.awaitMessage("download.done");
}
async function testDownload(options, localFile, expectedSize, description) {
let msg = await download(options);
equal(msg.status, "success", `downloads.download() works with ${description}`);
await waitForDownloads();
let localPath = downloadDir.clone();
let parts = Array.isArray(localFile) ? localFile : [localFile];
parts.map(p => localPath.append(p));
equal(localPath.fileSize, expectedSize, "Downloaded file has expected size");
localPath.remove(false);
}
yield extension.startup();
yield extension.awaitMessage("ready");
do_print("extension started");
// Call download() with just the url property.
yield testDownload({url: FILE_URL}, FILE_NAME, FILE_LEN, "just source");
// Call download() with a filename property.
yield testDownload({
url: FILE_URL,
filename: "newpath.txt",
}, "newpath.txt", FILE_LEN, "source and filename");
// Call download() with a filename with subdirs.
yield testDownload({
url: FILE_URL,
filename: "sub/dir/file",
}, ["sub", "dir", "file"], FILE_LEN, "source and filename with subdirs");
// Call download() with a filename with existing subdirs.
yield testDownload({
url: FILE_URL,
filename: "sub/dir/file2",
}, ["sub", "dir", "file2"], FILE_LEN, "source and filename with existing subdirs");
// Only run Windows path separator test on Windows.
if (WINDOWS) {
// Call download() with a filename with Windows path separator.
yield testDownload({
url: FILE_URL,
filename: "sub\\dir\\file3",
}, ["sub", "dir", "file3"], FILE_LEN, "filename with Windows path separator");
}
remove("sub", true);
// Call download(), filename with subdir, skipping parts.
yield testDownload({
url: FILE_URL,
filename: "skip//part",
}, ["skip", "part"], FILE_LEN, "source, filename, with subdir, skipping parts");
remove("skip", true);
// Check conflictAction of "uniquify".
touch(FILE_NAME);
yield testDownload({
url: FILE_URL,
conflictAction: "uniquify",
}, FILE_NAME_UNIQUE, FILE_LEN, "conflictAction=uniquify");
// todo check that preexisting file was not modified?
remove(FILE_NAME);
// Check conflictAction of "overwrite".
touch(FILE_NAME);
yield testDownload({
url: FILE_URL,
conflictAction: "overwrite",
}, FILE_NAME, FILE_LEN, "conflictAction=overwrite");
// Try to download in invalid url
yield download({url: "this is not a valid URL"}).then(msg => {
equal(msg.status, "error", "downloads.download() fails with invalid url");
ok(/not a valid URL/.test(msg.errmsg), "error message for invalid url is correct");
});
// Try to download to an empty path.
yield download({
url: FILE_URL,
filename: "",
}).then(msg => {
equal(msg.status, "error", "downloads.download() fails with empty filename");
equal(msg.errmsg, "filename must not be empty", "error message for empty filename is correct");
});
// Try to download to an absolute path.
const absolutePath = OS.Path.join(WINDOWS ? "\\tmp" : "/tmp", "file_download.txt");
yield download({
url: FILE_URL,
filename: absolutePath,
}).then(msg => {
equal(msg.status, "error", "downloads.download() fails with absolute filename");
equal(msg.errmsg, "filename must not be an absolute path", `error message for absolute path (${absolutePath}) is correct`);
});
if (WINDOWS) {
yield download({
url: FILE_URL,
filename: "C:\\file_download.txt",
}).then(msg => {
equal(msg.status, "error", "downloads.download() fails with absolute filename");
equal(msg.errmsg, "filename must not be an absolute path", "error message for absolute path with drive letter is correct");
});
}
// Try to download to a relative path containing ..
yield download({
url: FILE_URL,
filename: OS.Path.join("..", "file_download.txt"),
}).then(msg => {
equal(msg.status, "error", "downloads.download() fails with back-references");
equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
});
// Try to download to a long relative path containing ..
yield download({
url: FILE_URL,
filename: OS.Path.join("foo", "..", "..", "file_download.txt"),
}).then(msg => {
equal(msg.status, "error", "downloads.download() fails with back-references");
equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
});
// Try to download a blob url
const BLOB_STRING = "Hello, world";
yield testDownload({
blobme: [BLOB_STRING],
filename: FILE_NAME,
}, FILE_NAME, BLOB_STRING.length, "blob url");
extension.sendMessage("killTheBlob");
// Try to download a blob url without a given filename
yield testDownload({
blobme: [BLOB_STRING],
}, "download", BLOB_STRING.length, "blob url with no filename");
extension.sendMessage("killTheBlob");
yield extension.unload();
});
add_task(function* test_download_post() {
const server = createHttpServer();
const url = `http://localhost:${server.identity.primaryPort}/post-log`;
let received;
server.registerPathHandler("/post-log", request => {
received = request;
});
// Confirm received vs. expected values.
function confirm(method, headers = {}, body) {
equal(received.method, method, "method is correct");
for (let name in headers) {
ok(received.hasHeader(name), `header ${name} received`);
equal(received.getHeader(name), headers[name], `header ${name} is correct`);
}
if (body) {
const str = NetUtil.readInputStreamToString(received.bodyInputStream,
received.bodyInputStream.available());
equal(str, body, "body is correct");
}
}
function background() {
browser.test.onMessage.addListener(async options => {
try {
await browser.downloads.download(options);
} catch (err) {
browser.test.sendMessage("done", {err: err.message});
}
});
browser.downloads.onChanged.addListener(({state}) => {
if (state && state.current === "complete") {
browser.test.sendMessage("done", {ok: true});
}
});
}
const manifest = {permissions: ["downloads"]};
const extension = ExtensionTestUtils.loadExtension({background, manifest});
yield extension.startup();
function download(options) {
options.url = url;
options.conflictAction = "overwrite";
extension.sendMessage(options);
return extension.awaitMessage("done");
}
// Test method option.
let result = yield download({});
ok(result.ok, "download works without the method option, defaults to GET");
confirm("GET");
result = yield download({method: "PUT"});
ok(!result.ok, "download rejected with PUT method");
ok(/method: Invalid enumeration/.test(result.err), "descriptive error message");
result = yield download({method: "POST"});
ok(result.ok, "download works with POST method");
confirm("POST");
// Test body option values.
result = yield download({body: []});
ok(!result.ok, "download rejected because of non-string body");
ok(/body: Expected string/.test(result.err), "descriptive error message");
result = yield download({method: "POST", body: "of work"});
ok(result.ok, "download works with POST method and body");
confirm("POST", {"Content-Length": 7}, "of work");
// Test custom headers.
result = yield download({headers: [{name: "X-Custom"}]});
ok(!result.ok, "download rejected because of missing header value");
ok(/"value" is required/.test(result.err), "descriptive error message");
result = yield download({headers: [{name: "X-Custom", value: "13"}]});
ok(result.ok, "download works with a custom header");
confirm("GET", {"X-Custom": "13"});
// Test forbidden headers.
result = yield download({headers: [{name: "DNT", value: "1"}]});
ok(!result.ok, "download rejected because of forbidden header name DNT");
ok(/Forbidden request header/.test(result.err), "descriptive error message");
result = yield download({headers: [{name: "Proxy-Connection", value: "keep"}]});
ok(!result.ok, "download rejected because of forbidden header name prefix Proxy-");
ok(/Forbidden request header/.test(result.err), "descriptive error message");
result = yield download({headers: [{name: "Sec-ret", value: "13"}]});
ok(!result.ok, "download rejected because of forbidden header name prefix Sec-");
ok(/Forbidden request header/.test(result.err), "descriptive error message");
remove("post-log");
yield extension.unload();
});

View file

@ -0,0 +1,862 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://gre/modules/Downloads.jsm");
const server = createHttpServer();
server.registerDirectory("/data/", do_get_file("data"));
const ROOT = `http://localhost:${server.identity.primaryPort}`;
const BASE = `${ROOT}/data`;
const TXT_FILE = "file_download.txt";
const TXT_URL = BASE + "/" + TXT_FILE;
// Keep these in sync with code in interruptible.sjs
const INT_PARTIAL_LEN = 15;
const INT_TOTAL_LEN = 31;
const TEST_DATA = "This is 31 bytes of sample data";
const TOTAL_LEN = TEST_DATA.length;
const PARTIAL_LEN = 15;
// A handler to let us systematically test pausing/resuming/canceling
// of downloads. This target represents a small text file but a simple
// GET will stall after sending part of the data, to give the test code
// a chance to pause or do other operations on an in-progress download.
// A resumed download (ie, a GET with a Range: header) will allow the
// download to complete.
function handleRequest(request, response) {
response.setHeader("Content-Type", "text/plain", false);
if (request.hasHeader("Range")) {
let start, end;
let matches = request.getHeader("Range")
.match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
if (matches != null) {
start = matches[1] ? parseInt(matches[1], 10) : 0;
end = matches[2] ? parseInt(matches[2], 10) : (TOTAL_LEN - 1);
}
if (end == undefined || end >= TOTAL_LEN) {
response.setStatusLine(request.httpVersion, 416, "Requested Range Not Satisfiable");
response.setHeader("Content-Range", `*/${TOTAL_LEN}`, false);
response.finish();
return;
}
response.setStatusLine(request.httpVersion, 206, "Partial Content");
response.setHeader("Content-Range", `${start}-${end}/${TOTAL_LEN}`, false);
response.write(TEST_DATA.slice(start, end + 1));
} else {
response.processAsync();
response.setHeader("Content-Length", `${TOTAL_LEN}`, false);
response.write(TEST_DATA.slice(0, PARTIAL_LEN));
}
do_register_cleanup(() => {
try {
response.finish();
} catch (e) {
// This will throw, but we don't care at this point.
}
});
}
server.registerPathHandler("/interruptible.html", handleRequest);
let interruptibleCount = 0;
function getInterruptibleUrl() {
let n = interruptibleCount++;
return `${ROOT}/interruptible.html?count=${n}`;
}
function backgroundScript() {
let events = new Set();
let eventWaiter = null;
browser.downloads.onCreated.addListener(data => {
events.add({type: "onCreated", data});
if (eventWaiter) {
eventWaiter();
}
});
browser.downloads.onChanged.addListener(data => {
events.add({type: "onChanged", data});
if (eventWaiter) {
eventWaiter();
}
});
browser.downloads.onErased.addListener(data => {
events.add({type: "onErased", data});
if (eventWaiter) {
eventWaiter();
}
});
// Returns a promise that will resolve when the given list of expected
// events have all been seen. By default, succeeds only if the exact list
// of expected events is seen in the given order. options.exact can be
// set to false to allow other events and options.inorder can be set to
// false to allow the events to arrive in any order.
function waitForEvents(expected, options = {}) {
function compare(a, b) {
if (typeof b == "object" && b != null) {
if (typeof a != "object") {
return false;
}
return Object.keys(b).every(fld => compare(a[fld], b[fld]));
}
return (a == b);
}
const exact = ("exact" in options) ? options.exact : true;
const inorder = ("inorder" in options) ? options.inorder : true;
return new Promise((resolve, reject) => {
function check() {
function fail(msg) {
browser.test.fail(msg);
reject(new Error(msg));
}
if (events.size < expected.length) {
return;
}
if (exact && expected.length < events.size) {
fail(`Got ${events.size} events but only expected ${expected.length}`);
return;
}
let remaining = new Set(events);
if (inorder) {
for (let event of events) {
if (compare(event, expected[0])) {
expected.shift();
remaining.delete(event);
}
}
} else {
expected = expected.filter(val => {
for (let remainingEvent of remaining) {
if (compare(remainingEvent, val)) {
remaining.delete(remainingEvent);
return false;
}
}
return true;
});
}
// Events that did occur have been removed from expected so if
// expected is empty, we're done. If we didn't see all the
// expected events and we're not looking for an exact match,
// then we just may not have seen the event yet, so return without
// failing and check() will be called again when a new event arrives.
if (expected.length == 0) {
events = remaining;
eventWaiter = null;
resolve();
} else if (exact) {
fail(`Mismatched event: expecting ${JSON.stringify(expected[0])} but got ${JSON.stringify(Array.from(remaining)[0])}`);
}
}
eventWaiter = check;
check();
});
}
browser.test.onMessage.addListener(async (msg, ...args) => {
let match = msg.match(/(\w+).request$/);
if (!match) {
return;
}
let what = match[1];
if (what == "waitForEvents") {
try {
await waitForEvents(...args);
browser.test.sendMessage("waitForEvents.done", {status: "success"});
} catch (error) {
browser.test.sendMessage("waitForEvents.done", {status: "error", errmsg: error.message});
}
} else if (what == "clearEvents") {
events = new Set();
browser.test.sendMessage("clearEvents.done", {status: "success"});
} else {
try {
let result = await browser.downloads[what](...args);
browser.test.sendMessage(`${what}.done`, {status: "success", result});
} catch (error) {
browser.test.sendMessage(`${what}.done`, {status: "error", errmsg: error.message});
}
}
});
browser.test.sendMessage("ready");
}
let downloadDir;
let extension;
async function clearDownloads(callback) {
let list = await Downloads.getList(Downloads.ALL);
let downloads = await list.getAll();
await Promise.all(downloads.map(download => list.remove(download)));
return downloads;
}
function runInExtension(what, ...args) {
extension.sendMessage(`${what}.request`, ...args);
return extension.awaitMessage(`${what}.done`);
}
// This is pretty simplistic, it looks for a progress update for a
// download of the given url in which the total bytes are exactly equal
// to the given value. Unless you know exactly how data will arrive from
// the server (eg see interruptible.sjs), it probably isn't very useful.
async function waitForProgress(url, bytes) {
let list = await Downloads.getList(Downloads.ALL);
return new Promise(resolve => {
const view = {
onDownloadChanged(download) {
if (download.source.url == url && download.currentBytes == bytes) {
list.removeView(view);
resolve();
}
},
};
list.addView(view);
});
}
add_task(function* setup() {
const nsIFile = Ci.nsIFile;
downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
do_print(`downloadDir ${downloadDir.path}`);
Services.prefs.setIntPref("browser.download.folderList", 2);
Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir);
do_register_cleanup(() => {
Services.prefs.clearUserPref("browser.download.folderList");
Services.prefs.clearUserPref("browser.download.dir");
downloadDir.remove(true);
return clearDownloads();
});
yield clearDownloads().then(downloads => {
do_print(`removed ${downloads.length} pre-existing downloads from history`);
});
extension = ExtensionTestUtils.loadExtension({
background: backgroundScript,
manifest: {
permissions: ["downloads"],
},
});
yield extension.startup();
yield extension.awaitMessage("ready");
});
add_task(function* test_events() {
let msg = yield runInExtension("download", {url: TXT_URL});
equal(msg.status, "success", "download() succeeded");
const id = msg.result;
msg = yield runInExtension("waitForEvents", [
{type: "onCreated", data: {id, url: TXT_URL}},
{
type: "onChanged",
data: {
id,
state: {
previous: "in_progress",
current: "complete",
},
},
},
]);
equal(msg.status, "success", "got onCreated and onChanged events");
});
add_task(function* test_cancel() {
let url = getInterruptibleUrl();
do_print(url);
let msg = yield runInExtension("download", {url});
equal(msg.status, "success", "download() succeeded");
const id = msg.result;
let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
msg = yield runInExtension("waitForEvents", [
{type: "onCreated", data: {id}},
]);
equal(msg.status, "success", "got created and changed events");
yield progressPromise;
do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
msg = yield runInExtension("cancel", id);
equal(msg.status, "success", "cancel() succeeded");
// This sequence of events is bogus (bug 1256243)
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
state: {
previous: "in_progress",
current: "interrupted",
},
paused: {
previous: false,
current: true,
},
},
}, {
type: "onChanged",
data: {
id,
error: {
previous: null,
current: "USER_CANCELED",
},
},
}, {
type: "onChanged",
data: {
id,
paused: {
previous: true,
current: false,
},
},
},
]);
equal(msg.status, "success", "got onChanged events corresponding to cancel()");
msg = yield runInExtension("search", {error: "USER_CANCELED"});
equal(msg.status, "success", "search() succeeded");
equal(msg.result.length, 1, "search() found 1 download");
equal(msg.result[0].id, id, "download.id is correct");
equal(msg.result[0].state, "interrupted", "download.state is correct");
equal(msg.result[0].paused, false, "download.paused is correct");
equal(msg.result[0].canResume, false, "download.canResume is correct");
equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
equal(msg.result[0].exists, false, "download.exists is correct");
msg = yield runInExtension("pause", id);
equal(msg.status, "error", "cannot pause a canceled download");
msg = yield runInExtension("resume", id);
equal(msg.status, "error", "cannot resume a canceled download");
});
add_task(function* test_pauseresume() {
let url = getInterruptibleUrl();
let msg = yield runInExtension("download", {url});
equal(msg.status, "success", "download() succeeded");
const id = msg.result;
let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
msg = yield runInExtension("waitForEvents", [
{type: "onCreated", data: {id}},
]);
equal(msg.status, "success", "got created and changed events");
yield progressPromise;
do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
msg = yield runInExtension("pause", id);
equal(msg.status, "success", "pause() succeeded");
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
id,
state: {
previous: "in_progress",
current: "interrupted",
},
paused: {
previous: false,
current: true,
},
canResume: {
previous: false,
current: true,
},
},
}, {
type: "onChanged",
data: {
id,
error: {
previous: null,
current: "USER_CANCELED",
},
},
}]);
equal(msg.status, "success", "got onChanged event corresponding to pause");
msg = yield runInExtension("search", {paused: true});
equal(msg.status, "success", "search() succeeded");
equal(msg.result.length, 1, "search() found 1 download");
equal(msg.result[0].id, id, "download.id is correct");
equal(msg.result[0].state, "interrupted", "download.state is correct");
equal(msg.result[0].paused, true, "download.paused is correct");
equal(msg.result[0].canResume, true, "download.canResume is correct");
equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
equal(msg.result[0].exists, false, "download.exists is correct");
msg = yield runInExtension("search", {error: "USER_CANCELED"});
equal(msg.status, "success", "search() succeeded");
let found = msg.result.filter(item => item.id == id);
equal(found.length, 1, "search() by error found the paused download");
msg = yield runInExtension("pause", id);
equal(msg.status, "error", "cannot pause an already paused download");
msg = yield runInExtension("resume", id);
equal(msg.status, "success", "resume() succeeded");
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
id,
state: {
previous: "interrupted",
current: "in_progress",
},
paused: {
previous: true,
current: false,
},
canResume: {
previous: true,
current: false,
},
error: {
previous: "USER_CANCELED",
current: null,
},
},
},
{
type: "onChanged",
data: {
id,
state: {
previous: "in_progress",
current: "complete",
},
},
},
]);
equal(msg.status, "success", "got onChanged events for resume and complete");
msg = yield runInExtension("search", {id});
equal(msg.status, "success", "search() succeeded");
equal(msg.result.length, 1, "search() found 1 download");
equal(msg.result[0].state, "complete", "download.state is correct");
equal(msg.result[0].paused, false, "download.paused is correct");
equal(msg.result[0].canResume, false, "download.canResume is correct");
equal(msg.result[0].error, null, "download.error is correct");
equal(msg.result[0].bytesReceived, INT_TOTAL_LEN, "download.bytesReceived is correct");
equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
equal(msg.result[0].exists, true, "download.exists is correct");
msg = yield runInExtension("pause", id);
equal(msg.status, "error", "cannot pause a completed download");
msg = yield runInExtension("resume", id);
equal(msg.status, "error", "cannot resume a completed download");
});
add_task(function* test_pausecancel() {
let url = getInterruptibleUrl();
let msg = yield runInExtension("download", {url});
equal(msg.status, "success", "download() succeeded");
const id = msg.result;
let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
msg = yield runInExtension("waitForEvents", [
{type: "onCreated", data: {id}},
]);
equal(msg.status, "success", "got created and changed events");
yield progressPromise;
do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
msg = yield runInExtension("pause", id);
equal(msg.status, "success", "pause() succeeded");
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
id,
state: {
previous: "in_progress",
current: "interrupted",
},
paused: {
previous: false,
current: true,
},
canResume: {
previous: false,
current: true,
},
},
}, {
type: "onChanged",
data: {
id,
error: {
previous: null,
current: "USER_CANCELED",
},
},
}]);
equal(msg.status, "success", "got onChanged event corresponding to pause");
msg = yield runInExtension("search", {paused: true});
equal(msg.status, "success", "search() succeeded");
equal(msg.result.length, 1, "search() found 1 download");
equal(msg.result[0].id, id, "download.id is correct");
equal(msg.result[0].state, "interrupted", "download.state is correct");
equal(msg.result[0].paused, true, "download.paused is correct");
equal(msg.result[0].canResume, true, "download.canResume is correct");
equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
equal(msg.result[0].exists, false, "download.exists is correct");
msg = yield runInExtension("search", {error: "USER_CANCELED"});
equal(msg.status, "success", "search() succeeded");
let found = msg.result.filter(item => item.id == id);
equal(found.length, 1, "search() by error found the paused download");
msg = yield runInExtension("cancel", id);
equal(msg.status, "success", "cancel() succeeded");
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
id,
paused: {
previous: true,
current: false,
},
canResume: {
previous: true,
current: false,
},
},
},
]);
equal(msg.status, "success", "got onChanged event for cancel");
msg = yield runInExtension("search", {id});
equal(msg.status, "success", "search() succeeded");
equal(msg.result.length, 1, "search() found 1 download");
equal(msg.result[0].state, "interrupted", "download.state is correct");
equal(msg.result[0].paused, false, "download.paused is correct");
equal(msg.result[0].canResume, false, "download.canResume is correct");
equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
equal(msg.result[0].exists, false, "download.exists is correct");
});
add_task(function* test_pause_resume_cancel_badargs() {
let BAD_ID = 1000;
let msg = yield runInExtension("pause", BAD_ID);
equal(msg.status, "error", "pause() failed with a bad download id");
ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
msg = yield runInExtension("resume", BAD_ID);
equal(msg.status, "error", "resume() failed with a bad download id");
ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
msg = yield runInExtension("cancel", BAD_ID);
equal(msg.status, "error", "cancel() failed with a bad download id");
ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
});
add_task(function* test_file_removal() {
let msg = yield runInExtension("download", {url: TXT_URL});
equal(msg.status, "success", "download() succeeded");
const id = msg.result;
msg = yield runInExtension("waitForEvents", [
{type: "onCreated", data: {id, url: TXT_URL}},
{
type: "onChanged",
data: {
id,
state: {
previous: "in_progress",
current: "complete",
},
},
},
]);
equal(msg.status, "success", "got onCreated and onChanged events");
msg = yield runInExtension("removeFile", id);
equal(msg.status, "success", "removeFile() succeeded");
msg = yield runInExtension("removeFile", id);
equal(msg.status, "error", "removeFile() fails since the file was already removed.");
ok(/file doesn't exist/.test(msg.errmsg), "removeFile() failed on removed file.");
msg = yield runInExtension("removeFile", 1000);
ok(/Invalid download id/.test(msg.errmsg), "removeFile() failed due to non-existent id");
});
add_task(function* test_removal_of_incomplete_download() {
let url = getInterruptibleUrl();
let msg = yield runInExtension("download", {url});
equal(msg.status, "success", "download() succeeded");
const id = msg.result;
let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
msg = yield runInExtension("waitForEvents", [
{type: "onCreated", data: {id}},
]);
equal(msg.status, "success", "got created and changed events");
yield progressPromise;
do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
msg = yield runInExtension("pause", id);
equal(msg.status, "success", "pause() succeeded");
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
id,
state: {
previous: "in_progress",
current: "interrupted",
},
paused: {
previous: false,
current: true,
},
canResume: {
previous: false,
current: true,
},
},
}, {
type: "onChanged",
data: {
id,
error: {
previous: null,
current: "USER_CANCELED",
},
},
}]);
equal(msg.status, "success", "got onChanged event corresponding to pause");
msg = yield runInExtension("removeFile", id);
equal(msg.status, "error", "removeFile() on paused download failed");
ok(/Cannot remove incomplete download/.test(msg.errmsg), "removeFile() failed due to download being incomplete");
msg = yield runInExtension("resume", id);
equal(msg.status, "success", "resume() succeeded");
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
id,
state: {
previous: "interrupted",
current: "in_progress",
},
paused: {
previous: true,
current: false,
},
canResume: {
previous: true,
current: false,
},
error: {
previous: "USER_CANCELED",
current: null,
},
},
},
{
type: "onChanged",
data: {
id,
state: {
previous: "in_progress",
current: "complete",
},
},
},
]);
equal(msg.status, "success", "got onChanged events for resume and complete");
msg = yield runInExtension("removeFile", id);
equal(msg.status, "success", "removeFile() succeeded following completion of resumed download.");
});
// Test erase(). We don't do elaborate testing of the query handling
// since it uses the exact same engine as search() which is tested
// more thoroughly in test_chrome_ext_downloads_search.html
add_task(function* test_erase() {
yield clearDownloads();
yield runInExtension("clearEvents");
function* download() {
let msg = yield runInExtension("download", {url: TXT_URL});
equal(msg.status, "success", "download succeeded");
let id = msg.result;
msg = yield runInExtension("waitForEvents", [{
type: "onChanged", data: {id, state: {current: "complete"}},
}], {exact: false});
equal(msg.status, "success", "download finished");
return id;
}
let ids = {};
ids.dl1 = yield download();
ids.dl2 = yield download();
ids.dl3 = yield download();
let msg = yield runInExtension("search", {});
equal(msg.status, "success", "search succeded");
equal(msg.result.length, 3, "search found 3 downloads");
msg = yield runInExtension("clearEvents");
msg = yield runInExtension("erase", {id: ids.dl1});
equal(msg.status, "success", "erase by id succeeded");
msg = yield runInExtension("waitForEvents", [
{type: "onErased", data: ids.dl1},
]);
equal(msg.status, "success", "received onErased event");
msg = yield runInExtension("search", {});
equal(msg.status, "success", "search succeded");
equal(msg.result.length, 2, "search found 2 downloads");
msg = yield runInExtension("erase", {});
equal(msg.status, "success", "erase everything succeeded");
msg = yield runInExtension("waitForEvents", [
{type: "onErased", data: ids.dl2},
{type: "onErased", data: ids.dl3},
], {inorder: false});
equal(msg.status, "success", "received 2 onErased events");
msg = yield runInExtension("search", {});
equal(msg.status, "success", "search succeded");
equal(msg.result.length, 0, "search found 0 downloads");
});
function loadImage(img, data) {
return new Promise((resolve) => {
img.src = data;
img.onload = resolve;
});
}
add_task(function* test_getFileIcon() {
let webNav = Services.appShell.createWindowlessBrowser(false);
let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
let system = Services.scriptSecurityManager.getSystemPrincipal();
docShell.createAboutBlankContentViewer(system);
let img = webNav.document.createElement("img");
let msg = yield runInExtension("download", {url: TXT_URL});
equal(msg.status, "success", "download() succeeded");
const id = msg.result;
msg = yield runInExtension("getFileIcon", id);
equal(msg.status, "success", "getFileIcon() succeeded");
yield loadImage(img, msg.result);
equal(img.height, 32, "returns an icon with the right height");
equal(img.width, 32, "returns an icon with the right width");
msg = yield runInExtension("waitForEvents", [
{type: "onCreated", data: {id, url: TXT_URL}},
{type: "onChanged"},
]);
equal(msg.status, "success", "got events");
msg = yield runInExtension("getFileIcon", id);
equal(msg.status, "success", "getFileIcon() succeeded");
yield loadImage(img, msg.result);
equal(img.height, 32, "returns an icon with the right height after download");
equal(img.width, 32, "returns an icon with the right width after download");
msg = yield runInExtension("getFileIcon", id + 100);
equal(msg.status, "error", "getFileIcon() failed");
ok(msg.errmsg.includes("Invalid download id"), "download id is invalid");
msg = yield runInExtension("getFileIcon", id, {size: 127});
equal(msg.status, "success", "getFileIcon() succeeded");
yield loadImage(img, msg.result);
equal(img.height, 127, "returns an icon with the right custom height");
equal(img.width, 127, "returns an icon with the right custom width");
msg = yield runInExtension("getFileIcon", id, {size: 1});
equal(msg.status, "success", "getFileIcon() succeeded");
yield loadImage(img, msg.result);
equal(img.height, 1, "returns an icon with the right custom height");
equal(img.width, 1, "returns an icon with the right custom width");
msg = yield runInExtension("getFileIcon", id, {size: "foo"});
equal(msg.status, "error", "getFileIcon() fails");
ok(msg.errmsg.includes("Error processing size"), "size is not a number");
msg = yield runInExtension("getFileIcon", id, {size: 0});
equal(msg.status, "error", "getFileIcon() fails");
ok(msg.errmsg.includes("Error processing size"), "size is too small");
msg = yield runInExtension("getFileIcon", id, {size: 128});
equal(msg.status, "error", "getFileIcon() fails");
ok(msg.errmsg.includes("Error processing size"), "size is too big");
webNav.close();
});
add_task(function* cleanup() {
yield extension.unload();
});

View file

@ -0,0 +1,402 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://gre/modules/Downloads.jsm");
const server = createHttpServer();
server.registerDirectory("/data/", do_get_file("data"));
const BASE = `http://localhost:${server.identity.primaryPort}/data`;
const TXT_FILE = "file_download.txt";
const TXT_URL = BASE + "/" + TXT_FILE;
const TXT_LEN = 46;
const HTML_FILE = "file_download.html";
const HTML_URL = BASE + "/" + HTML_FILE;
const HTML_LEN = 117;
const BIG_LEN = 1000; // something bigger both TXT_LEN and HTML_LEN
function backgroundScript() {
let complete = new Map();
function waitForComplete(id) {
if (complete.has(id)) {
return complete.get(id).promise;
}
let promise = new Promise(resolve => {
complete.set(id, {resolve});
});
complete.get(id).promise = promise;
return promise;
}
browser.downloads.onChanged.addListener(change => {
if (change.state && change.state.current == "complete") {
// Make sure we have a promise.
waitForComplete(change.id);
complete.get(change.id).resolve();
}
});
browser.test.onMessage.addListener(async (msg, ...args) => {
if (msg == "download.request") {
try {
let id = await browser.downloads.download(args[0]);
browser.test.sendMessage("download.done", {status: "success", id});
} catch (error) {
browser.test.sendMessage("download.done", {status: "error", errmsg: error.message});
}
} else if (msg == "search.request") {
try {
let downloads = await browser.downloads.search(args[0]);
browser.test.sendMessage("search.done", {status: "success", downloads});
} catch (error) {
browser.test.sendMessage("search.done", {status: "error", errmsg: error.message});
}
} else if (msg == "waitForComplete.request") {
await waitForComplete(args[0]);
browser.test.sendMessage("waitForComplete.done");
}
});
browser.test.sendMessage("ready");
}
async function clearDownloads(callback) {
let list = await Downloads.getList(Downloads.ALL);
let downloads = await list.getAll();
await Promise.all(downloads.map(download => list.remove(download)));
return downloads;
}
add_task(function* test_search() {
const nsIFile = Ci.nsIFile;
let downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
do_print(`downloadDir ${downloadDir.path}`);
function downloadPath(filename) {
let path = downloadDir.clone();
path.append(filename);
return path.path;
}
Services.prefs.setIntPref("browser.download.folderList", 2);
Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir);
do_register_cleanup(async () => {
Services.prefs.clearUserPref("browser.download.folderList");
Services.prefs.clearUserPref("browser.download.dir");
await cleanupDir(downloadDir);
await clearDownloads();
});
yield clearDownloads().then(downloads => {
do_print(`removed ${downloads.length} pre-existing downloads from history`);
});
let extension = ExtensionTestUtils.loadExtension({
background: backgroundScript,
manifest: {
permissions: ["downloads"],
},
});
async function download(options) {
extension.sendMessage("download.request", options);
let result = await extension.awaitMessage("download.done");
if (result.status == "success") {
do_print(`wait for onChanged event to indicate ${result.id} is complete`);
extension.sendMessage("waitForComplete.request", result.id);
await extension.awaitMessage("waitForComplete.done");
}
return result;
}
function search(query) {
extension.sendMessage("search.request", query);
return extension.awaitMessage("search.done");
}
yield extension.startup();
yield extension.awaitMessage("ready");
// Do some downloads...
const time1 = new Date();
let downloadIds = {};
let msg = yield download({url: TXT_URL});
equal(msg.status, "success", "download() succeeded");
downloadIds.txt1 = msg.id;
const TXT_FILE2 = "NewFile.txt";
msg = yield download({url: TXT_URL, filename: TXT_FILE2});
equal(msg.status, "success", "download() succeeded");
downloadIds.txt2 = msg.id;
const time2 = new Date();
msg = yield download({url: HTML_URL});
equal(msg.status, "success", "download() succeeded");
downloadIds.html1 = msg.id;
const HTML_FILE2 = "renamed.html";
msg = yield download({url: HTML_URL, filename: HTML_FILE2});
equal(msg.status, "success", "download() succeeded");
downloadIds.html2 = msg.id;
const time3 = new Date();
// Search for each individual download and check
// the corresponding DownloadItem.
function* checkDownloadItem(id, expect) {
let item = yield search({id});
equal(item.status, "success", "search() succeeded");
equal(item.downloads.length, 1, "search() found exactly 1 download");
Object.keys(expect).forEach(function(field) {
equal(item.downloads[0][field], expect[field], `DownloadItem.${field} is correct"`);
});
}
yield checkDownloadItem(downloadIds.txt1, {
url: TXT_URL,
filename: downloadPath(TXT_FILE),
mime: "text/plain",
state: "complete",
bytesReceived: TXT_LEN,
totalBytes: TXT_LEN,
fileSize: TXT_LEN,
exists: true,
});
yield checkDownloadItem(downloadIds.txt2, {
url: TXT_URL,
filename: downloadPath(TXT_FILE2),
mime: "text/plain",
state: "complete",
bytesReceived: TXT_LEN,
totalBytes: TXT_LEN,
fileSize: TXT_LEN,
exists: true,
});
yield checkDownloadItem(downloadIds.html1, {
url: HTML_URL,
filename: downloadPath(HTML_FILE),
mime: "text/html",
state: "complete",
bytesReceived: HTML_LEN,
totalBytes: HTML_LEN,
fileSize: HTML_LEN,
exists: true,
});
yield checkDownloadItem(downloadIds.html2, {
url: HTML_URL,
filename: downloadPath(HTML_FILE2),
mime: "text/html",
state: "complete",
bytesReceived: HTML_LEN,
totalBytes: HTML_LEN,
fileSize: HTML_LEN,
exists: true,
});
function* checkSearch(query, expected, description, exact) {
let item = yield search(query);
equal(item.status, "success", "search() succeeded");
equal(item.downloads.length, expected.length, `search() for ${description} found exactly ${expected.length} downloads`);
let receivedIds = item.downloads.map(i => i.id);
if (exact) {
receivedIds.forEach((id, idx) => {
equal(id, downloadIds[expected[idx]], `search() for ${description} returned ${expected[idx]} in position ${idx}`);
});
} else {
Object.keys(downloadIds).forEach(key => {
const id = downloadIds[key];
const thisExpected = expected.includes(key);
equal(receivedIds.includes(id), thisExpected,
`search() for ${description} ${thisExpected ? "includes" : "does not include"} ${key}`);
});
}
}
// Check that search with an invalid id returns nothing.
// NB: for now ids are not persistent and we start numbering them at 1
// so a sufficiently large number will be unused.
const INVALID_ID = 1000;
yield checkSearch({id: INVALID_ID}, [], "invalid id");
// Check that search on url works.
yield checkSearch({url: TXT_URL}, ["txt1", "txt2"], "url");
// Check that regexp on url works.
const HTML_REGEX = "[downlad]{8}\.html+$";
yield checkSearch({urlRegex: HTML_REGEX}, ["html1", "html2"], "url regexp");
// Check that compatible url+regexp works
yield checkSearch({url: HTML_URL, urlRegex: HTML_REGEX}, ["html1", "html2"], "compatible url+urlRegex");
// Check that incompatible url+regexp works
yield checkSearch({url: TXT_URL, urlRegex: HTML_REGEX}, [], "incompatible url+urlRegex");
// Check that search on filename works.
yield checkSearch({filename: downloadPath(TXT_FILE)}, ["txt1"], "filename");
// Check that regexp on filename works.
yield checkSearch({filenameRegex: HTML_REGEX}, ["html1"], "filename regex");
// Check that compatible filename+regexp works
yield checkSearch({filename: downloadPath(HTML_FILE), filenameRegex: HTML_REGEX}, ["html1"], "compatible filename+filename regex");
// Check that incompatible filename+regexp works
yield checkSearch({filename: downloadPath(TXT_FILE), filenameRegex: HTML_REGEX}, [], "incompatible filename+filename regex");
// Check that simple positive search terms work.
yield checkSearch({query: ["file_download"]}, ["txt1", "txt2", "html1", "html2"],
"term file_download");
yield checkSearch({query: ["NewFile"]}, ["txt2"], "term NewFile");
// Check that positive search terms work case-insensitive.
yield checkSearch({query: ["nEwfILe"]}, ["txt2"], "term nEwfiLe");
// Check that negative search terms work.
yield checkSearch({query: ["-txt"]}, ["html1", "html2"], "term -txt");
// Check that positive and negative search terms together work.
yield checkSearch({query: ["html", "-renamed"]}, ["html1"], "postive and negative terms");
function* checkSearchWithDate(query, expected, description) {
const fields = Object.keys(query);
if (fields.length != 1 || !(query[fields[0]] instanceof Date)) {
throw new Error("checkSearchWithDate expects exactly one Date field");
}
const field = fields[0];
const date = query[field];
let newquery = {};
// Check as a Date
newquery[field] = date;
yield checkSearch(newquery, expected, `${description} as Date`);
// Check as numeric milliseconds
newquery[field] = date.valueOf();
yield checkSearch(newquery, expected, `${description} as numeric ms`);
// Check as stringified milliseconds
newquery[field] = date.valueOf().toString();
yield checkSearch(newquery, expected, `${description} as string ms`);
// Check as ISO string
newquery[field] = date.toISOString();
yield checkSearch(newquery, expected, `${description} as iso string`);
}
// Check startedBefore
yield checkSearchWithDate({startedBefore: time1}, [], "before time1");
yield checkSearchWithDate({startedBefore: time2}, ["txt1", "txt2"], "before time2");
yield checkSearchWithDate({startedBefore: time3}, ["txt1", "txt2", "html1", "html2"], "before time3");
// Check startedAfter
yield checkSearchWithDate({startedAfter: time1}, ["txt1", "txt2", "html1", "html2"], "after time1");
yield checkSearchWithDate({startedAfter: time2}, ["html1", "html2"], "after time2");
yield checkSearchWithDate({startedAfter: time3}, [], "after time3");
// Check simple search on totalBytes
yield checkSearch({totalBytes: TXT_LEN}, ["txt1", "txt2"], "totalBytes");
yield checkSearch({totalBytes: HTML_LEN}, ["html1", "html2"], "totalBytes");
// Check simple test on totalBytes{Greater,Less}
// (NB: TXT_LEN < HTML_LEN < BIG_LEN)
yield checkSearch({totalBytesGreater: 0}, ["txt1", "txt2", "html1", "html2"], "totalBytesGreater than 0");
yield checkSearch({totalBytesGreater: TXT_LEN}, ["html1", "html2"], `totalBytesGreater than ${TXT_LEN}`);
yield checkSearch({totalBytesGreater: HTML_LEN}, [], `totalBytesGreater than ${HTML_LEN}`);
yield checkSearch({totalBytesLess: TXT_LEN}, [], `totalBytesLess than ${TXT_LEN}`);
yield checkSearch({totalBytesLess: HTML_LEN}, ["txt1", "txt2"], `totalBytesLess than ${HTML_LEN}`);
yield checkSearch({totalBytesLess: BIG_LEN}, ["txt1", "txt2", "html1", "html2"], `totalBytesLess than ${BIG_LEN}`);
// Check good combinations of totalBytes*.
yield checkSearch({totalBytes: HTML_LEN, totalBytesGreater: TXT_LEN}, ["html1", "html2"], "totalBytes and totalBytesGreater");
yield checkSearch({totalBytes: TXT_LEN, totalBytesLess: HTML_LEN}, ["txt1", "txt2"], "totalBytes and totalBytesGreater");
yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: BIG_LEN, totalBytesGreater: 0}, ["html1", "html2"], "totalBytes and totalBytesLess and totalBytesGreater");
// Check bad combination of totalBytes*.
yield checkSearch({totalBytesLess: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytesLess, totalBytesGreater combination");
yield checkSearch({totalBytes: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytes, totalBytesGreater combination");
yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: TXT_LEN}, [], "bad totalBytes, totalBytesLess combination");
// Check mime.
yield checkSearch({mime: "text/plain"}, ["txt1", "txt2"], "mime text/plain");
yield checkSearch({mime: "text/html"}, ["html1", "html2"], "mime text/htmlplain");
yield checkSearch({mime: "video/webm"}, [], "mime video/webm");
// Check fileSize.
yield checkSearch({fileSize: TXT_LEN}, ["txt1", "txt2"], "fileSize");
yield checkSearch({fileSize: HTML_LEN}, ["html1", "html2"], "fileSize");
// Fields like bytesReceived, paused, state, exists are meaningful
// for downloads that are in progress but have not yet completed.
// todo: add tests for these when we have better support for in-progress
// downloads (e.g., after pause(), resume() and cancel() are implemented)
// Check multiple query properties.
// We could make this testing arbitrarily complicated...
// We already tested combining fields with obvious interactions above
// (e.g., filename and filenameRegex or startTime and startedBefore/After)
// so now just throw as many fields as we can at a single search and
// make sure a simple case still works.
yield checkSearch({
url: TXT_URL,
urlRegex: "download",
filename: downloadPath(TXT_FILE),
filenameRegex: "download",
query: ["download"],
startedAfter: time1.valueOf().toString(),
startedBefore: time2.valueOf().toString(),
totalBytes: TXT_LEN,
totalBytesGreater: 0,
totalBytesLess: BIG_LEN,
mime: "text/plain",
fileSize: TXT_LEN,
}, ["txt1"], "many properties");
// Check simple orderBy (forward and backward).
yield checkSearch({orderBy: ["startTime"]}, ["txt1", "txt2", "html1", "html2"], "orderBy startTime", true);
yield checkSearch({orderBy: ["-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy -startTime", true);
// Check orderBy with multiple fields.
// NB: TXT_URL and HTML_URL differ only in extension and .html precedes .txt
yield checkSearch({orderBy: ["url", "-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy with multiple fields", true);
// Check orderBy with limit.
yield checkSearch({orderBy: ["url"], limit: 1}, ["html1"], "orderBy with limit", true);
// Check bad arguments.
function* checkBadSearch(query, pattern, description) {
let item = yield search(query);
equal(item.status, "error", "search() failed");
ok(pattern.test(item.errmsg), `error message for ${description} was correct (${item.errmsg}).`);
}
yield checkBadSearch("myquery", /Incorrect argument type/, "query is not an object");
yield checkBadSearch({bogus: "boo"}, /Unexpected property/, "query contains an unknown field");
yield checkBadSearch({query: "query string"}, /Expected array/, "query.query is a string");
yield checkBadSearch({startedBefore: "i am not a time"}, /Type error/, "query.startedBefore is not a valid time");
yield checkBadSearch({startedAfter: "i am not a time"}, /Type error/, "query.startedAfter is not a valid time");
yield checkBadSearch({endedBefore: "i am not a time"}, /Type error/, "query.endedBefore is not a valid time");
yield checkBadSearch({endedAfter: "i am not a time"}, /Type error/, "query.endedAfter is not a valid time");
yield checkBadSearch({urlRegex: "["}, /Invalid urlRegex/, "query.urlRegexp is not a valid regular expression");
yield checkBadSearch({filenameRegex: "["}, /Invalid filenameRegex/, "query.filenameRegexp is not a valid regular expression");
yield checkBadSearch({orderBy: "startTime"}, /Expected array/, "query.orderBy is not an array");
yield checkBadSearch({orderBy: ["bogus"]}, /Invalid orderBy field/, "query.orderBy references a non-existent field");
yield extension.unload();
});

View file

@ -0,0 +1,175 @@
"use strict";
/* globals browser */
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
function promiseAddonStartup() {
const {Management} = Cu.import("resource://gre/modules/Extension.jsm");
return new Promise(resolve => {
let listener = (evt, extension) => {
Management.off("startup", listener);
resolve(extension);
};
Management.on("startup", listener);
});
}
add_task(function* setup() {
yield ExtensionTestUtils.startAddonManager();
});
add_task(function* test_experiments_api() {
let apiAddonFile = Extension.generateZipFile({
"install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest"
em:id="meh@experiments.addons.mozilla.org"
em:name="Meh Experiment"
em:type="256"
em:version="0.1"
em:description="Meh experiment"
em:creator="Mozilla">
<em:targetApplication>
<Description
em:id="xpcshell@tests.mozilla.org"
em:minVersion="48"
em:maxVersion="*"/>
</em:targetApplication>
</Description>
</RDF>
`,
"api.js": String.raw`
Components.utils.import("resource://gre/modules/Services.jsm");
Services.obs.notifyObservers(null, "webext-api-loaded", "");
class API extends ExtensionAPI {
getAPI(context) {
return {
meh: {
hello(text) {
Services.obs.notifyObservers(null, "webext-api-hello", text);
}
}
}
}
}
`,
"schema.json": [
{
"namespace": "meh",
"description": "All full of meh.",
"permissions": ["experiments.meh"],
"functions": [
{
"name": "hello",
"type": "function",
"description": "Hates you. This is all.",
"parameters": [
{"type": "string", "name": "text"},
],
},
],
},
],
});
let addonFile = Extension.generateXPI({
manifest: {
applications: {gecko: {id: "meh@web.extension"}},
permissions: ["experiments.meh"],
},
background() {
// The test code below checks that hello() is called at the right
// time with the string "Here I am". Verify that the api schema is
// being correctly interpreted by calling hello() with bad arguments
// and only calling hello() with the magic string if the call with
// bad arguments throws.
try {
browser.meh.hello("I should not see this", "since two arguments are bad");
} catch (err) {
browser.meh.hello("Here I am");
}
},
});
let boringAddonFile = Extension.generateXPI({
manifest: {
applications: {gecko: {id: "boring@web.extension"}},
},
background() {
if (browser.meh) {
browser.meh.hello("Here I should not be");
}
},
});
do_register_cleanup(() => {
for (let file of [apiAddonFile, addonFile, boringAddonFile]) {
Services.obs.notifyObservers(file, "flush-cache-entry", null);
file.remove(false);
}
});
let resolveHello;
let observer = (subject, topic, data) => {
if (topic == "webext-api-loaded") {
ok(!!resolveHello, "Should not see API loaded until dependent extension loads");
} else if (topic == "webext-api-hello") {
resolveHello(data);
}
};
Services.obs.addObserver(observer, "webext-api-loaded", false);
Services.obs.addObserver(observer, "webext-api-hello", false);
do_register_cleanup(() => {
Services.obs.removeObserver(observer, "webext-api-loaded");
Services.obs.removeObserver(observer, "webext-api-hello");
});
// Install API add-on.
let apiAddon = yield AddonManager.installTemporaryAddon(apiAddonFile);
let {APIs} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {});
ok(APIs.apis.has("meh"), "Should have meh API.");
// Install boring WebExtension add-on.
let boringAddon = yield AddonManager.installTemporaryAddon(boringAddonFile);
yield promiseAddonStartup();
// Install interesting WebExtension add-on.
let promise = new Promise(resolve => {
resolveHello = resolve;
});
let addon = yield AddonManager.installTemporaryAddon(addonFile);
yield promiseAddonStartup();
let hello = yield promise;
equal(hello, "Here I am", "Should get hello from add-on");
// Cleanup.
apiAddon.uninstall();
boringAddon.userDisabled = true;
yield new Promise(do_execute_soon);
equal(addon.appDisabled, true, "Add-on should be app-disabled after its dependency is removed.");
addon.uninstall();
boringAddon.uninstall();
});

View file

@ -0,0 +1,55 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_is_allowed_incognito_access() {
async function background() {
let allowed = await browser.extension.isAllowedIncognitoAccess();
browser.test.assertEq(true, allowed, "isAllowedIncognitoAccess is true");
browser.test.notifyPass("isAllowedIncognitoAccess");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {},
});
yield extension.startup();
yield extension.awaitFinish("isAllowedIncognitoAccess");
yield extension.unload();
});
add_task(function* test_in_incognito_context_false() {
function background() {
browser.test.assertEq(false, browser.extension.inIncognitoContext, "inIncognitoContext returned false");
browser.test.notifyPass("inIncognitoContext");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {},
});
yield extension.startup();
yield extension.awaitFinish("inIncognitoContext");
yield extension.unload();
});
add_task(function* test_is_allowed_file_scheme_access() {
async function background() {
let allowed = await browser.extension.isAllowedFileSchemeAccess();
browser.test.assertEq(false, allowed, "isAllowedFileSchemeAccess is false");
browser.test.notifyPass("isAllowedFileSchemeAccess");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {},
});
yield extension.startup();
yield extension.awaitFinish("isAllowedFileSchemeAccess");
yield extension.unload();
});

View file

@ -0,0 +1,202 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://testing-common/MockRegistrar.jsm");
let idleService = {
_observers: new Set(),
_activity: {
addCalls: [],
removeCalls: [],
observerFires: [],
},
_reset: function() {
this._observers.clear();
this._activity.addCalls = [];
this._activity.removeCalls = [];
this._activity.observerFires = [];
},
_fireObservers: function(state) {
for (let observer of this._observers.values()) {
observer.observe(observer, state, null);
this._activity.observerFires.push(state);
}
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIIdleService]),
idleTime: 19999,
addIdleObserver: function(observer, time) {
this._observers.add(observer);
this._activity.addCalls.push(time);
},
removeIdleObserver: function(observer, time) {
this._observers.delete(observer);
this._activity.removeCalls.push(time);
},
};
function checkActivity(expectedActivity) {
let {expectedAdd, expectedRemove, expectedFires} = expectedActivity;
let {addCalls, removeCalls, observerFires} = idleService._activity;
equal(expectedAdd.length, addCalls.length, "idleService.addIdleObserver was called the expected number of times");
equal(expectedRemove.length, removeCalls.length, "idleService.removeIdleObserver was called the expected number of times");
equal(expectedFires.length, observerFires.length, "idle observer was fired the expected number of times");
deepEqual(addCalls, expectedAdd, "expected interval passed to idleService.addIdleObserver");
deepEqual(removeCalls, expectedRemove, "expected interval passed to idleService.removeIdleObserver");
deepEqual(observerFires, expectedFires, "expected topic passed to idle observer");
}
add_task(function* setup() {
let fakeIdleService = MockRegistrar.register("@mozilla.org/widget/idleservice;1", idleService);
do_register_cleanup(() => {
MockRegistrar.unregister(fakeIdleService);
});
});
add_task(function* testQueryStateActive() {
function background() {
browser.idle.queryState(20).then(status => {
browser.test.assertEq("active", status, "Idle status is active");
browser.test.notifyPass("idle");
},
err => {
browser.test.fail(`Error: ${err} :: ${err.stack}`);
browser.test.notifyFail("idle");
});
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
permissions: ["idle"],
},
});
yield extension.startup();
yield extension.awaitFinish("idle");
yield extension.unload();
});
add_task(function* testQueryStateIdle() {
function background() {
browser.idle.queryState(15).then(status => {
browser.test.assertEq("idle", status, "Idle status is idle");
browser.test.notifyPass("idle");
},
err => {
browser.test.fail(`Error: ${err} :: ${err.stack}`);
browser.test.notifyFail("idle");
});
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
permissions: ["idle"],
},
});
yield extension.startup();
yield extension.awaitFinish("idle");
yield extension.unload();
});
add_task(function* testOnlySetDetectionInterval() {
function background() {
browser.idle.setDetectionInterval(99);
browser.test.sendMessage("detectionIntervalSet");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
permissions: ["idle"],
},
});
idleService._reset();
yield extension.startup();
yield extension.awaitMessage("detectionIntervalSet");
idleService._fireObservers("idle");
checkActivity({expectedAdd: [], expectedRemove: [], expectedFires: []});
yield extension.unload();
});
add_task(function* testSetDetectionIntervalBeforeAddingListener() {
function background() {
browser.idle.setDetectionInterval(99);
browser.idle.onStateChanged.addListener(newState => {
browser.test.assertEq("idle", newState, "listener fired with the expected state");
browser.test.sendMessage("listenerFired");
});
browser.test.sendMessage("listenerAdded");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
permissions: ["idle"],
},
});
idleService._reset();
yield extension.startup();
yield extension.awaitMessage("listenerAdded");
idleService._fireObservers("idle");
yield extension.awaitMessage("listenerFired");
checkActivity({expectedAdd: [99], expectedRemove: [], expectedFires: ["idle"]});
yield extension.unload();
});
add_task(function* testSetDetectionIntervalAfterAddingListener() {
function background() {
browser.idle.onStateChanged.addListener(newState => {
browser.test.assertEq("idle", newState, "listener fired with the expected state");
browser.test.sendMessage("listenerFired");
});
browser.idle.setDetectionInterval(99);
browser.test.sendMessage("detectionIntervalSet");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
permissions: ["idle"],
},
});
idleService._reset();
yield extension.startup();
yield extension.awaitMessage("detectionIntervalSet");
idleService._fireObservers("idle");
yield extension.awaitMessage("listenerFired");
checkActivity({expectedAdd: [60, 99], expectedRemove: [60], expectedFires: ["idle"]});
yield extension.unload();
});
add_task(function* testOnlyAddingListener() {
function background() {
browser.idle.onStateChanged.addListener(newState => {
browser.test.assertEq("active", newState, "listener fired with the expected state");
browser.test.sendMessage("listenerFired");
});
browser.test.sendMessage("listenerAdded");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
permissions: ["idle"],
},
});
idleService._reset();
yield extension.startup();
yield extension.awaitMessage("listenerAdded");
idleService._fireObservers("active");
yield extension.awaitMessage("listenerFired");
// check that "idle-daily" topic does not cause a listener to fire
idleService._fireObservers("idle-daily");
checkActivity({expectedAdd: [60], expectedRemove: [], expectedFires: ["active", "idle-daily"]});
yield extension.unload();
});

View file

@ -0,0 +1,37 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_json_parser() {
const ID = "json@test.web.extension";
let xpi = Extension.generateXPI({
files: {
"manifest.json": String.raw`{
// This is a manifest.
"applications": {"gecko": {"id": "${ID}"}},
"name": "This \" is // not a comment",
"version": "0.1\\" // , "description": "This is not a description"
}`,
},
});
let expectedManifest = {
"applications": {"gecko": {"id": ID}},
"name": "This \" is // not a comment",
"version": "0.1\\",
};
let fileURI = Services.io.newFileURI(xpi);
let uri = NetUtil.newURI(`jar:${fileURI.spec}!/`);
let extension = new ExtensionData(uri);
yield extension.readManifest();
Assert.deepEqual(extension.rawManifest, expectedManifest,
"Manifest with correctly-filtered comments");
Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
xpi.remove(false);
});

View file

@ -0,0 +1,168 @@
"use strict";
/* globals browser */
Cu.import("resource://gre/modules/Extension.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {LegacyExtensionContext} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
/**
* This test case ensures that LegacyExtensionContext instances:
* - expose the expected API object and can join the messaging
* of a webextension given its addon id
* - the exposed API object can receive a port related to a `runtime.connect`
* Port created in the webextension's background page
* - the received Port instance can exchange messages with the background page
* - the received Port receive a disconnect event when the webextension is
* shutting down
*/
add_task(function* test_legacy_extension_context() {
function background() {
let bgURL = window.location.href;
let extensionInfo = {
bgURL,
// Extract the assigned uuid from the background page url.
uuid: window.location.hostname,
};
browser.test.sendMessage("webextension-ready", extensionInfo);
let port;
browser.test.onMessage.addListener(async msg => {
if (msg == "do-send-message") {
let reply = await browser.runtime.sendMessage("webextension -> legacy_extension message");
browser.test.assertEq("legacy_extension -> webextension reply", reply,
"Got the expected message from the LegacyExtensionContext");
browser.test.sendMessage("got-reply-message");
} else if (msg == "do-connect") {
port = browser.runtime.connect();
port.onMessage.addListener(portMsg => {
browser.test.assertEq("legacy_extension -> webextension port message", portMsg,
"Got the expected message from the LegacyExtensionContext");
port.postMessage("webextension -> legacy_extension port message");
});
} else if (msg == "do-disconnect") {
port.disconnect();
}
});
}
let extensionData = {
background,
};
let extension = Extension.generate(extensionData);
let waitForExtensionInfo = new Promise((resolve, reject) => {
extension.on("test-message", function testMessageListener(kind, msg, ...args) {
if (msg != "webextension-ready") {
reject(new Error(`Got an unexpected test-message: ${msg}`));
} else {
extension.off("test-message", testMessageListener);
resolve(args[0]);
}
});
});
// Connect to the target extension as an external context
// using the given custom sender info.
let legacyContext;
extension.on("startup", function onStartup() {
extension.off("startup", onStartup);
legacyContext = new LegacyExtensionContext(extension);
extension.callOnClose({
close: () => legacyContext.unload(),
});
});
yield extension.startup();
let extensionInfo = yield waitForExtensionInfo;
equal(legacyContext.envType, "legacy_extension",
"LegacyExtensionContext instance has the expected type");
ok(legacyContext.api, "Got the expected API object");
ok(legacyContext.api.browser, "Got the expected browser property");
let waitMessage = new Promise(resolve => {
const {browser} = legacyContext.api;
browser.runtime.onMessage.addListener((singleMsg, msgSender) => {
resolve({singleMsg, msgSender});
// Send a reply to the sender.
return Promise.resolve("legacy_extension -> webextension reply");
});
});
extension.testMessage("do-send-message");
let {singleMsg, msgSender} = yield waitMessage;
equal(singleMsg, "webextension -> legacy_extension message",
"Got the expected message");
ok(msgSender, "Got a message sender object");
equal(msgSender.id, extensionInfo.uuid, "The sender has the expected id property");
equal(msgSender.url, extensionInfo.bgURL, "The sender has the expected url property");
// Wait confirmation that the reply has been received.
yield new Promise((resolve, reject) => {
extension.on("test-message", function testMessageListener(kind, msg, ...args) {
if (msg != "got-reply-message") {
reject(new Error(`Got an unexpected test-message: ${msg}`));
} else {
extension.off("test-message", testMessageListener);
resolve();
}
});
});
let waitConnectPort = new Promise(resolve => {
let {browser} = legacyContext.api;
browser.runtime.onConnect.addListener(port => {
resolve(port);
});
});
extension.testMessage("do-connect");
let port = yield waitConnectPort;
ok(port, "Got the Port API object");
ok(port.sender, "The port has a sender property");
equal(port.sender.id, extensionInfo.uuid,
"The port sender has the expected id property");
equal(port.sender.url, extensionInfo.bgURL,
"The port sender has the expected url property");
let waitPortMessage = new Promise(resolve => {
port.onMessage.addListener((msg) => {
resolve(msg);
});
});
port.postMessage("legacy_extension -> webextension port message");
let msg = yield waitPortMessage;
equal(msg, "webextension -> legacy_extension port message",
"LegacyExtensionContext received the expected message from the webextension");
let waitForDisconnect = new Promise(resolve => {
port.onDisconnect.addListener(resolve);
});
extension.testMessage("do-disconnect");
yield waitForDisconnect;
do_print("Got the disconnect event on unload");
yield extension.shutdown();
});

View file

@ -0,0 +1,188 @@
"use strict";
/* globals browser */
Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
// Import EmbeddedExtensionManager to be able to check that the
// tacked instances are cleared after the embedded extension shutdown.
const {
EmbeddedExtensionManager,
} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm", {});
/**
* This test case ensures that the LegacyExtensionsUtils.EmbeddedExtension:
* - load the embedded webextension resources from a "/webextension/" dir
* inside the XPI.
* - EmbeddedExtension.prototype.api returns an API object which exposes
* a working `runtime.onConnect` event object (e.g. the API can receive a port
* when the embedded webextension is started and it can exchange messages
* with the background page).
* - EmbeddedExtension.prototype.startup/shutdown methods manage the embedded
* webextension lifecycle as expected.
*/
add_task(function* test_embedded_webextension_utils() {
function backgroundScript() {
let port = browser.runtime.connect();
port.onMessage.addListener((msg) => {
if (msg == "legacy_extension -> webextension") {
port.postMessage("webextension -> legacy_extension");
port.disconnect();
}
});
}
const id = "@test.embedded.web.extension";
// Extensions.generateXPI is used here (and in the other hybrid addons tests in this same
// test dir) to be able to generate an xpi with the directory layout that we expect from
// an hybrid legacy+webextension addon (where all the embedded webextension resources are
// loaded from a 'webextension/' directory).
let fakeHybridAddonFile = Extension.generateZipFile({
"webextension/manifest.json": {
applications: {gecko: {id}},
name: "embedded webextension name",
manifest_version: 2,
version: "1.0",
background: {
scripts: ["bg.js"],
},
},
"webextension/bg.js": `new ${backgroundScript}`,
});
// Remove the generated xpi file and flush the its jar cache
// on cleanup.
do_register_cleanup(() => {
Services.obs.notifyObservers(fakeHybridAddonFile, "flush-cache-entry", null);
fakeHybridAddonFile.remove(false);
});
let fileURI = Services.io.newFileURI(fakeHybridAddonFile);
let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`, null, null);
let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
id, resourceURI,
});
ok(embeddedExtension, "Got the embeddedExtension object");
equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
"Got the expected number of tracked embedded extension instances");
do_print("waiting embeddedExtension.startup");
let embeddedExtensionAPI = yield embeddedExtension.startup();
ok(embeddedExtensionAPI, "Got the embeddedExtensionAPI object");
let waitConnectPort = new Promise(resolve => {
let {browser} = embeddedExtensionAPI;
browser.runtime.onConnect.addListener(port => {
resolve(port);
});
});
let port = yield waitConnectPort;
ok(port, "Got the Port API object");
let waitPortMessage = new Promise(resolve => {
port.onMessage.addListener((msg) => {
resolve(msg);
});
});
port.postMessage("legacy_extension -> webextension");
let msg = yield waitPortMessage;
equal(msg, "webextension -> legacy_extension",
"LegacyExtensionContext received the expected message from the webextension");
let waitForDisconnect = new Promise(resolve => {
port.onDisconnect.addListener(resolve);
});
do_print("Wait for the disconnect port event");
yield waitForDisconnect;
do_print("Got the disconnect port event");
yield embeddedExtension.shutdown();
equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
"EmbeddedExtension instances has been untracked from the EmbeddedExtensionManager");
});
function* createManifestErrorTestCase(id, xpi, expectedError) {
// Remove the generated xpi file and flush the its jar cache
// on cleanup.
do_register_cleanup(() => {
Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
xpi.remove(false);
});
let fileURI = Services.io.newFileURI(xpi);
let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`, null, null);
let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
id, resourceURI,
});
yield Assert.rejects(embeddedExtension.startup(), expectedError,
"embedded extension startup rejected");
// Shutdown a "never-started" addon with an embedded webextension should not
// raise any exception, and if it does this test will fail.
yield embeddedExtension.shutdown();
}
add_task(function* test_startup_error_empty_manifest() {
const id = "empty-manifest@test.embedded.web.extension";
const files = {
"webextension/manifest.json": ``,
};
const expectedError = "(NS_BASE_STREAM_CLOSED)";
let fakeHybridAddonFile = Extension.generateZipFile(files);
yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
});
add_task(function* test_startup_error_invalid_json_manifest() {
const id = "invalid-json-manifest@test.embedded.web.extension";
const files = {
"webextension/manifest.json": `{ "name": }`,
};
const expectedError = "JSON.parse:";
let fakeHybridAddonFile = Extension.generateZipFile(files);
yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
});
add_task(function* test_startup_error_blocking_validation_errors() {
const id = "blocking-manifest-validation-error@test.embedded.web.extension";
const files = {
"webextension/manifest.json": {
name: "embedded webextension name",
manifest_version: 2,
version: "1.0",
background: {
scripts: {},
},
},
};
function expectedError(actual) {
if (actual.errors && actual.errors.length == 1 &&
actual.errors[0].startsWith("Reading manifest:")) {
return true;
}
return false;
}
let fakeHybridAddonFile = Extension.generateZipFile(files);
yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
});

View file

@ -0,0 +1,50 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
function backgroundScript() {
let hasRun = localStorage.getItem("has-run");
let result;
if (!hasRun) {
localStorage.setItem("has-run", "yup");
localStorage.setItem("test-item", "item1");
result = "item1";
} else {
let data = localStorage.getItem("test-item");
if (data == "item1") {
localStorage.setItem("test-item", "item2");
result = "item2";
} else if (data == "item2") {
localStorage.removeItem("test-item");
result = "deleted";
} else if (!data) {
localStorage.clear();
result = "cleared";
}
}
browser.test.sendMessage("result", result);
browser.test.notifyPass("localStorage");
}
const ID = "test-webextension@mozilla.com";
let extensionData = {
manifest: {applications: {gecko: {id: ID}}},
background: backgroundScript,
};
add_task(function* test_localStorage() {
const RESULTS = ["item1", "item2", "deleted", "cleared", "item1"];
for (let expected of RESULTS) {
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
let actual = yield extension.awaitMessage("result");
yield extension.awaitFinish("localStorage");
yield extension.unload();
equal(actual, expected, "got expected localStorage data");
}
});

View file

@ -0,0 +1,20 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_management_schema() {
function background() {
browser.test.assertTrue(browser.management, "browser.management API exists");
browser.test.notifyPass("management-schema");
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["management"],
},
background: `(${background})()`,
});
yield extension.startup();
yield extension.awaitFinish("management-schema");
yield extension.unload();
});

View file

@ -0,0 +1,135 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://gre/modules/AddonManager.jsm");
Cu.import("resource://testing-common/AddonTestUtils.jsm");
Cu.import("resource://testing-common/MockRegistrar.jsm");
const {promiseAddonByID} = AddonTestUtils;
const id = "uninstall_self_test@tests.mozilla.com";
const manifest = {
applications: {
gecko: {
id,
},
},
name: "test extension name",
version: "1.0",
};
const waitForUninstalled = () => new Promise(resolve => {
const listener = {
onUninstalled: (addon) => {
equal(addon.id, id, "The expected add-on has been uninstalled");
AddonManager.getAddonByID(addon.id, checkedAddon => {
equal(checkedAddon, null, "Add-on no longer exists");
AddonManager.removeAddonListener(listener);
resolve();
});
},
};
AddonManager.addAddonListener(listener);
});
let promptService = {
_response: null,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptService]),
confirmEx: function(...args) {
this._confirmExArgs = args;
return this._response;
},
};
add_task(function* setup() {
let fakePromptService = MockRegistrar.register("@mozilla.org/embedcomp/prompt-service;1", promptService);
do_register_cleanup(() => {
MockRegistrar.unregister(fakePromptService);
});
yield ExtensionTestUtils.startAddonManager();
});
add_task(function* test_management_uninstall_no_prompt() {
function background() {
browser.test.onMessage.addListener(msg => {
browser.management.uninstallSelf();
});
}
let extension = ExtensionTestUtils.loadExtension({
manifest,
background,
useAddonManager: "temporary",
});
yield extension.startup();
let addon = yield promiseAddonByID(id);
notEqual(addon, null, "Add-on is installed");
extension.sendMessage("uninstall");
yield waitForUninstalled();
yield extension.markUnloaded();
Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
});
add_task(function* test_management_uninstall_prompt_uninstall() {
promptService._response = 0;
function background() {
browser.test.onMessage.addListener(msg => {
browser.management.uninstallSelf({showConfirmDialog: true});
});
}
let extension = ExtensionTestUtils.loadExtension({
manifest,
background,
useAddonManager: "temporary",
});
yield extension.startup();
let addon = yield promiseAddonByID(id);
notEqual(addon, null, "Add-on is installed");
extension.sendMessage("uninstall");
yield waitForUninstalled();
yield extension.markUnloaded();
// Test localization strings
equal(promptService._confirmExArgs[1], `Uninstall ${manifest.name}`);
equal(promptService._confirmExArgs[2],
`The extension “${manifest.name}” is requesting to be uninstalled. What would you like to do?`);
equal(promptService._confirmExArgs[4], "Uninstall");
equal(promptService._confirmExArgs[5], "Keep Installed");
Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
});
add_task(function* test_management_uninstall_prompt_keep() {
promptService._response = 1;
function background() {
browser.test.onMessage.addListener(async msg => {
await browser.test.assertRejects(
browser.management.uninstallSelf({showConfirmDialog: true}),
"User cancelled uninstall of extension",
"Expected rejection when user declines uninstall");
browser.test.sendMessage("uninstall-rejected");
});
}
let extension = ExtensionTestUtils.loadExtension({
manifest,
background,
useAddonManager: "temporary",
});
yield extension.startup();
let addon = yield promiseAddonByID(id);
notEqual(addon, null, "Add-on is installed");
extension.sendMessage("uninstall");
yield extension.awaitMessage("uninstall-rejected");
addon = yield promiseAddonByID(id);
notEqual(addon, null, "Add-on remains installed");
yield extension.unload();
Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
});

View file

@ -0,0 +1,30 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_manifest_csp() {
let normalized = yield ExtensionTestUtils.normalizeManifest({
"content_security_policy": "script-src 'self'; object-src 'none'",
});
equal(normalized.error, undefined, "Should not have an error");
equal(normalized.errors.length, 0, "Should not have warnings");
equal(normalized.value.content_security_policy,
"script-src 'self'; object-src 'none'",
"Should have the expected poilcy string");
normalized = yield ExtensionTestUtils.normalizeManifest({
"content_security_policy": "object-src 'none'",
});
equal(normalized.error, undefined, "Should not have an error");
Assert.deepEqual(normalized.errors,
["Error processing content_security_policy: SyntaxError: Policy is missing a required \u2018script-src\u2019 directive"],
"Should have the expected warning");
equal(normalized.value.content_security_policy, null,
"Invalid policy string should be omitted");
});

View file

@ -0,0 +1,27 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_manifest_incognito() {
let normalized = yield ExtensionTestUtils.normalizeManifest({
"incognito": "spanning",
});
equal(normalized.error, undefined, "Should not have an error");
equal(normalized.errors.length, 0, "Should not have warnings");
equal(normalized.value.incognito,
"spanning",
"Should have the expected incognito string");
normalized = yield ExtensionTestUtils.normalizeManifest({
"incognito": "split",
});
equal(normalized.error, undefined, "Should not have an error");
Assert.deepEqual(normalized.errors,
['Error processing incognito: Invalid enumeration value "split"'],
"Should have the expected warning");
equal(normalized.value.incognito, null,
"Invalid incognito string should be omitted");
});

View file

@ -0,0 +1,13 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_manifest_minimum_chrome_version() {
let normalized = yield ExtensionTestUtils.normalizeManifest({
"minimum_chrome_version": "42",
});
equal(normalized.error, undefined, "Should not have an error");
equal(normalized.errors.length, 0, "Should not have warnings");
});

View file

@ -0,0 +1,514 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
/* globals chrome */
const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes";
const ECHO_BODY = String.raw`
import struct
import sys
while True:
rawlen = sys.stdin.read(4)
if len(rawlen) == 0:
sys.exit(0)
msglen = struct.unpack('@I', rawlen)[0]
msg = sys.stdin.read(msglen)
sys.stdout.write(struct.pack('@I', msglen))
sys.stdout.write(msg)
`;
const INFO_BODY = String.raw`
import json
import os
import struct
import sys
msg = json.dumps({"args": sys.argv, "cwd": os.getcwd()})
sys.stdout.write(struct.pack('@I', len(msg)))
sys.stdout.write(msg)
sys.exit(0)
`;
const STDERR_LINES = ["hello stderr", "this should be a separate line"];
let STDERR_MSG = STDERR_LINES.join("\\n");
const STDERR_BODY = String.raw`
import sys
sys.stderr.write("${STDERR_MSG}")
`;
const SCRIPTS = [
{
name: "echo",
description: "a native app that echoes back messages it receives",
script: ECHO_BODY.replace(/^ {2}/gm, ""),
},
{
name: "info",
description: "a native app that gives some info about how it was started",
script: INFO_BODY.replace(/^ {2}/gm, ""),
},
{
name: "stderr",
description: "a native app that writes to stderr and then exits",
script: STDERR_BODY.replace(/^ {2}/gm, ""),
},
];
add_task(function* setup() {
yield setupHosts(SCRIPTS);
});
// Test the basic operation of native messaging with a simple
// script that echoes back whatever message is sent to it.
add_task(function* test_happy_path() {
function background() {
let port = browser.runtime.connectNative("echo");
port.onMessage.addListener(msg => {
browser.test.sendMessage("message", msg);
});
browser.test.onMessage.addListener((what, payload) => {
if (what == "send") {
if (payload._json) {
let json = payload._json;
payload.toJSON = () => json;
delete payload._json;
}
port.postMessage(payload);
}
});
browser.test.sendMessage("ready");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
applications: {gecko: {id: ID}},
permissions: ["nativeMessaging"],
},
});
yield extension.startup();
yield extension.awaitMessage("ready");
const tests = [
{
data: "this is a string",
what: "simple string",
},
{
data: "Это юникода",
what: "unicode string",
},
{
data: {test: "hello"},
what: "simple object",
},
{
data: {
what: "An object with a few properties",
number: 123,
bool: true,
nested: {what: "another object"},
},
what: "object with several properties",
},
{
data: {
ignoreme: true,
_json: {data: "i have a tojson method"},
},
expected: {data: "i have a tojson method"},
what: "object with toJSON() method",
},
];
for (let test of tests) {
extension.sendMessage("send", test.data);
let response = yield extension.awaitMessage("message");
let expected = test.expected || test.data;
deepEqual(response, expected, `Echoed a message of type ${test.what}`);
}
let procCount = yield getSubprocessCount();
equal(procCount, 1, "subprocess is still running");
let exitPromise = waitForSubprocessExit();
yield extension.unload();
yield exitPromise;
});
if (AppConstants.platform == "win") {
// "relative.echo" has a relative path in the host manifest.
add_task(function* test_relative_path() {
function background() {
let port = browser.runtime.connectNative("relative.echo");
let MSG = "test relative echo path";
port.onMessage.addListener(msg => {
browser.test.assertEq(MSG, msg, "Got expected message back");
browser.test.sendMessage("done");
});
port.postMessage(MSG);
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
applications: {gecko: {id: ID}},
permissions: ["nativeMessaging"],
},
});
yield extension.startup();
yield extension.awaitMessage("done");
let procCount = yield getSubprocessCount();
equal(procCount, 1, "subprocess is still running");
let exitPromise = waitForSubprocessExit();
yield extension.unload();
yield exitPromise;
});
}
// Test sendNativeMessage()
add_task(function* test_sendNativeMessage() {
async function background() {
let MSG = {test: "hello world"};
// Check error handling
await browser.test.assertRejects(
browser.runtime.sendNativeMessage("nonexistent", MSG),
/Attempt to postMessage on disconnected port/,
"sendNativeMessage() to a nonexistent app failed");
// Check regular message exchange
let reply = await browser.runtime.sendNativeMessage("echo", MSG);
let expected = JSON.stringify(MSG);
let received = JSON.stringify(reply);
browser.test.assertEq(expected, received, "Received echoed native message");
browser.test.sendMessage("finished");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
applications: {gecko: {id: ID}},
permissions: ["nativeMessaging"],
},
});
yield extension.startup();
yield extension.awaitMessage("finished");
// With sendNativeMessage(), the subprocess should be disconnected
// after exchanging a single message.
yield waitForSubprocessExit();
yield extension.unload();
});
// Test calling Port.disconnect()
add_task(function* test_disconnect() {
function background() {
let port = browser.runtime.connectNative("echo");
port.onMessage.addListener((msg, msgPort) => {
browser.test.assertEq(port, msgPort, "onMessage handler should receive the port as the second argument");
browser.test.sendMessage("message", msg);
});
port.onDisconnect.addListener(msgPort => {
browser.test.fail("onDisconnect should not be called for disconnect()");
});
browser.test.onMessage.addListener((what, payload) => {
if (what == "send") {
if (payload._json) {
let json = payload._json;
payload.toJSON = () => json;
delete payload._json;
}
port.postMessage(payload);
} else if (what == "disconnect") {
try {
port.disconnect();
browser.test.sendMessage("disconnect-result", {success: true});
} catch (err) {
browser.test.sendMessage("disconnect-result", {
success: false,
errmsg: err.message,
});
}
}
});
browser.test.sendMessage("ready");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
applications: {gecko: {id: ID}},
permissions: ["nativeMessaging"],
},
});
yield extension.startup();
yield extension.awaitMessage("ready");
extension.sendMessage("send", "test");
let response = yield extension.awaitMessage("message");
equal(response, "test", "Echoed a string");
let procCount = yield getSubprocessCount();
equal(procCount, 1, "subprocess is running");
extension.sendMessage("disconnect");
response = yield extension.awaitMessage("disconnect-result");
equal(response.success, true, "disconnect succeeded");
do_print("waiting for subprocess to exit");
yield waitForSubprocessExit();
procCount = yield getSubprocessCount();
equal(procCount, 0, "subprocess is no longer running");
extension.sendMessage("disconnect");
response = yield extension.awaitMessage("disconnect-result");
equal(response.success, true, "second call to disconnect silently ignored");
yield extension.unload();
});
// Test the limit on message size for writing
add_task(function* test_write_limit() {
Services.prefs.setIntPref(PREF_MAX_WRITE, 10);
function clearPref() {
Services.prefs.clearUserPref(PREF_MAX_WRITE);
}
do_register_cleanup(clearPref);
function background() {
const PAYLOAD = "0123456789A";
let port = browser.runtime.connectNative("echo");
try {
port.postMessage(PAYLOAD);
browser.test.sendMessage("result", null);
} catch (ex) {
browser.test.sendMessage("result", ex.message);
}
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
applications: {gecko: {id: ID}},
permissions: ["nativeMessaging"],
},
});
yield extension.startup();
let errmsg = yield extension.awaitMessage("result");
notEqual(errmsg, null, "native postMessage() failed for overly large message");
yield extension.unload();
yield waitForSubprocessExit();
clearPref();
});
// Test the limit on message size for reading
add_task(function* test_read_limit() {
Services.prefs.setIntPref(PREF_MAX_READ, 10);
function clearPref() {
Services.prefs.clearUserPref(PREF_MAX_READ);
}
do_register_cleanup(clearPref);
function background() {
const PAYLOAD = "0123456789A";
let port = browser.runtime.connectNative("echo");
port.onDisconnect.addListener(msgPort => {
browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument");
browser.test.assertEq("Native application tried to send a message of 13 bytes, which exceeds the limit of 10 bytes.", port.error && port.error.message);
browser.test.sendMessage("result", "disconnected");
});
port.onMessage.addListener(msg => {
browser.test.sendMessage("result", "message");
});
port.postMessage(PAYLOAD);
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
applications: {gecko: {id: ID}},
permissions: ["nativeMessaging"],
},
});
yield extension.startup();
let result = yield extension.awaitMessage("result");
equal(result, "disconnected", "native port disconnected on receiving large message");
yield extension.unload();
yield waitForSubprocessExit();
clearPref();
});
// Test that an extension without the nativeMessaging permission cannot
// use native messaging.
add_task(function* test_ext_permission() {
function background() {
browser.test.assertFalse("connectNative" in chrome.runtime, "chrome.runtime.connectNative does not exist without nativeMessaging permission");
browser.test.assertFalse("connectNative" in browser.runtime, "browser.runtime.connectNative does not exist without nativeMessaging permission");
browser.test.assertFalse("sendNativeMessage" in chrome.runtime, "chrome.runtime.sendNativeMessage does not exist without nativeMessaging permission");
browser.test.assertFalse("sendNativeMessage" in browser.runtime, "browser.runtime.sendNativeMessage does not exist without nativeMessaging permission");
browser.test.sendMessage("finished");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {},
});
yield extension.startup();
yield extension.awaitMessage("finished");
yield extension.unload();
});
// Test that an extension that is not listed in allowed_extensions for
// a native application cannot use that application.
add_task(function* test_app_permission() {
function background() {
let port = browser.runtime.connectNative("echo");
port.onDisconnect.addListener(msgPort => {
browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument");
browser.test.assertEq("This extension does not have permission to use native application echo (or the application is not installed)", port.error && port.error.message);
browser.test.sendMessage("result", "disconnected");
});
port.onMessage.addListener(msg => {
browser.test.sendMessage("result", "message");
});
port.postMessage({test: "test"});
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
permissions: ["nativeMessaging"],
},
}, "somethingelse@tests.mozilla.org");
yield extension.startup();
let result = yield extension.awaitMessage("result");
equal(result, "disconnected", "connectNative() failed without native app permission");
yield extension.unload();
let procCount = yield getSubprocessCount();
equal(procCount, 0, "No child process was started");
});
// Test that the command-line arguments and working directory for the
// native application are as expected.
add_task(function* test_child_process() {
function background() {
let port = browser.runtime.connectNative("info");
port.onMessage.addListener(msg => {
browser.test.sendMessage("result", msg);
});
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
applications: {gecko: {id: ID}},
permissions: ["nativeMessaging"],
},
});
yield extension.startup();
let msg = yield extension.awaitMessage("result");
equal(msg.args.length, 2, "Received one command line argument");
equal(msg.args[1], getPath("info.json"), "Command line argument is the path to the native host manifest");
equal(msg.cwd.replace(/^\/private\//, "/"), tmpDir.path,
"Working directory is the directory containing the native appliation");
let exitPromise = waitForSubprocessExit();
yield extension.unload();
yield exitPromise;
});
add_task(function* test_stderr() {
function background() {
let port = browser.runtime.connectNative("stderr");
port.onDisconnect.addListener(msgPort => {
browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument");
browser.test.assertEq(null, port.error, "Normal application exit is not an error");
browser.test.sendMessage("finished");
});
}
let {messages} = yield promiseConsoleOutput(function* () {
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
applications: {gecko: {id: ID}},
permissions: ["nativeMessaging"],
},
});
yield extension.startup();
yield extension.awaitMessage("finished");
yield extension.unload();
yield waitForSubprocessExit();
});
let lines = STDERR_LINES.map(line => messages.findIndex(msg => msg.message.includes(line)));
notEqual(lines[0], -1, "Saw first line of stderr output on the console");
notEqual(lines[1], -1, "Saw second line of stderr output on the console");
notEqual(lines[0], lines[1], "Stderr output lines are separated in the console");
});
// Test that calling connectNative() multiple times works
// (bug 1313980 was a previous regression in this area)
add_task(function* test_multiple_connects() {
async function background() {
function once() {
return new Promise(resolve => {
let MSG = "hello";
let port = browser.runtime.connectNative("echo");
port.onMessage.addListener(msg => {
browser.test.assertEq(MSG, msg, "Got expected message back");
port.disconnect();
resolve();
});
port.postMessage(MSG);
});
}
await once();
await once();
browser.test.notifyPass("multiple-connect");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
applications: {gecko: {id: ID}},
permissions: ["nativeMessaging"],
},
});
yield extension.startup();
yield extension.awaitFinish("multiple-connect");
yield extension.unload();
});

View file

@ -0,0 +1,128 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
"resource://testing-common/MockRegistry.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Subprocess.jsm");
const MAX_ROUND_TRIP_TIME_MS = AppConstants.DEBUG || AppConstants.ASAN ? 36 : 18;
const MAX_RETRIES = 5;
const ECHO_BODY = String.raw`
import struct
import sys
while True:
rawlen = sys.stdin.read(4)
if len(rawlen) == 0:
sys.exit(0)
msglen = struct.unpack('@I', rawlen)[0]
msg = sys.stdin.read(msglen)
sys.stdout.write(struct.pack('@I', msglen))
sys.stdout.write(msg)
`;
const SCRIPTS = [
{
name: "echo",
description: "A native app that echoes back messages it receives",
script: ECHO_BODY.replace(/^ {2}/gm, ""),
},
];
add_task(function* setup() {
yield setupHosts(SCRIPTS);
});
add_task(function* test_round_trip_perf() {
let extension = ExtensionTestUtils.loadExtension({
background() {
browser.test.onMessage.addListener(msg => {
if (msg != "run-tests") {
return;
}
let port = browser.runtime.connectNative("echo");
function next() {
port.postMessage({
"Lorem": {
"ipsum": {
"dolor": [
"sit amet",
"consectetur adipiscing elit",
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
],
"Ut enim": [
"ad minim veniam",
"quis nostrud exercitation ullamco",
"laboris nisi ut aliquip ex ea commodo consequat.",
],
"Duis": [
"aute irure dolor in reprehenderit in",
"voluptate velit esse cillum dolore eu",
"fugiat nulla pariatur.",
],
"Excepteur": [
"sint occaecat cupidatat non proident",
"sunt in culpa qui officia deserunt",
"mollit anim id est laborum.",
],
},
},
});
}
const COUNT = 1000;
let now;
function finish() {
let roundTripTime = (Date.now() - now) / COUNT;
port.disconnect();
browser.test.sendMessage("result", roundTripTime);
}
let count = 0;
port.onMessage.addListener(() => {
if (count == 0) {
// Skip the first round, since it includes the time it takes
// the app to start up.
now = Date.now();
}
if (count++ <= COUNT) {
next();
} else {
finish();
}
});
next();
});
},
manifest: {
applications: {gecko: {id: ID}},
permissions: ["nativeMessaging"],
},
});
yield extension.startup();
let roundTripTime = Infinity;
for (let i = 0; i < MAX_RETRIES && roundTripTime > MAX_ROUND_TRIP_TIME_MS; i++) {
extension.sendMessage("run-tests");
roundTripTime = yield extension.awaitMessage("result");
}
yield extension.unload();
ok(roundTripTime <= MAX_ROUND_TRIP_TIME_MS,
`Expected round trip time (${roundTripTime}ms) to be less than ${MAX_ROUND_TRIP_TIME_MS}ms`);
});

View file

@ -0,0 +1,82 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const WONTDIE_BODY = String.raw`
import signal
import struct
import sys
import time
signal.signal(signal.SIGTERM, signal.SIG_IGN)
def spin():
while True:
try:
signal.pause()
except AttributeError:
time.sleep(5)
while True:
rawlen = sys.stdin.read(4)
if len(rawlen) == 0:
spin()
msglen = struct.unpack('@I', rawlen)[0]
msg = sys.stdin.read(msglen)
sys.stdout.write(struct.pack('@I', msglen))
sys.stdout.write(msg)
`;
const SCRIPTS = [
{
name: "wontdie",
description: "a native app that does not exit when stdin closes or on SIGTERM",
script: WONTDIE_BODY.replace(/^ {2}/gm, ""),
},
];
add_task(function* setup() {
yield setupHosts(SCRIPTS);
});
// Test that an unresponsive native application still gets killed eventually
add_task(function* test_unresponsive_native_app() {
// XXX expose GRACEFUL_SHUTDOWN_TIME as a pref and reduce it
// just for this test?
function background() {
let port = browser.runtime.connectNative("wontdie");
const MSG = "echo me";
// bounce a message to make sure the process actually starts
port.onMessage.addListener(msg => {
browser.test.assertEq(msg, MSG, "Received echoed message");
browser.test.sendMessage("ready");
});
port.postMessage(MSG);
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
applications: {gecko: {id: ID}},
permissions: ["nativeMessaging"],
},
});
yield extension.startup();
yield extension.awaitMessage("ready");
let procCount = yield getSubprocessCount();
equal(procCount, 1, "subprocess is running");
let exitPromise = waitForSubprocessExit();
yield extension.unload();
yield exitPromise;
procCount = yield getSubprocessCount();
equal(procCount, 0, "subprocess was succesfully killed");
});

View file

@ -0,0 +1,30 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
function backgroundScript() {
function listener() {
browser.test.notifyFail("listener should not be invoked");
}
browser.runtime.onMessage.addListener(listener);
browser.runtime.onMessage.removeListener(listener);
browser.runtime.sendMessage("hello");
// Make sure that, if we somehow fail to remove the listener, then we'll run
// the listener before the test is marked as passing.
setTimeout(function() {
browser.test.notifyPass("onmessage_removelistener");
}, 0);
}
let extensionData = {
background: backgroundScript,
};
add_task(function* test_contentscript() {
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitFinish("onmessage_removelistener");
yield extension.unload();
});

View file

@ -0,0 +1,23 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_connect_without_listener() {
function background() {
let port = browser.runtime.connect();
port.onDisconnect.addListener(() => {
browser.test.assertEq("Could not establish connection. Receiving end does not exist.", port.error && port.error.message);
browser.test.notifyPass("port.onDisconnect was called");
});
}
let extensionData = {
background,
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitFinish("port.onDisconnect was called");
yield extension.unload();
});

View file

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
add_task(function* setup() {
ExtensionTestUtils.mockAppInfo();
});
add_task(function* test_getBrowserInfo() {
async function background() {
let info = await browser.runtime.getBrowserInfo();
browser.test.assertEq(info.name, "XPCShell", "name is valid");
browser.test.assertEq(info.vendor, "Mozilla", "vendor is Mozilla");
browser.test.assertEq(info.version, "48", "version is correct");
browser.test.assertEq(info.buildID, "20160315", "buildID is correct");
browser.test.notifyPass("runtime.getBrowserInfo");
}
const extension = ExtensionTestUtils.loadExtension({background});
yield extension.startup();
yield extension.awaitFinish("runtime.getBrowserInfo");
yield extension.unload();
});

View file

@ -0,0 +1,25 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
function backgroundScript() {
browser.runtime.getPlatformInfo(info => {
let validOSs = ["mac", "win", "android", "cros", "linux", "openbsd"];
let validArchs = ["arm", "x86-32", "x86-64"];
browser.test.assertTrue(validOSs.indexOf(info.os) != -1, "OS is valid");
browser.test.assertTrue(validArchs.indexOf(info.arch) != -1, "Architecture is valid");
browser.test.notifyPass("runtime.getPlatformInfo");
});
}
let extensionData = {
background: backgroundScript,
};
add_task(function* test_contentscript() {
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitFinish("runtime.getPlatformInfo");
yield extension.unload();
});

View file

@ -0,0 +1,337 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
XPCOMUtils.defineLazyGetter(this, "Management", () => {
const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
return Management;
});
const {
createAppInfo,
createTempWebExtensionFile,
promiseAddonByID,
promiseAddonEvent,
promiseCompleteAllInstalls,
promiseFindAddonUpdates,
promiseRestartManager,
promiseShutdownManager,
promiseStartupManager,
} = AddonTestUtils;
AddonTestUtils.init(this);
// Allow for unsigned addons.
AddonTestUtils.overrideCertDB();
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
function awaitEvent(eventName) {
return new Promise(resolve => {
let listener = (_eventName, ...args) => {
if (_eventName === eventName) {
Management.off(eventName, listener);
resolve(...args);
}
};
Management.on(eventName, listener);
});
}
function background() {
let onInstalledDetails = null;
let onStartupFired = false;
browser.runtime.onInstalled.addListener(details => {
onInstalledDetails = details;
});
browser.runtime.onStartup.addListener(() => {
onStartupFired = true;
});
browser.test.onMessage.addListener(message => {
if (message === "get-on-installed-details") {
onInstalledDetails = onInstalledDetails || {fired: false};
browser.test.sendMessage("on-installed-details", onInstalledDetails);
} else if (message === "did-on-startup-fire") {
browser.test.sendMessage("on-startup-fired", onStartupFired);
} else if (message === "reload-extension") {
browser.runtime.reload();
}
});
browser.runtime.onUpdateAvailable.addListener(details => {
browser.test.sendMessage("reloading");
browser.runtime.reload();
});
}
function* expectEvents(extension, {onStartupFired, onInstalledFired, onInstalledReason}) {
extension.sendMessage("get-on-installed-details");
let details = yield extension.awaitMessage("on-installed-details");
if (onInstalledFired) {
equal(details.reason, onInstalledReason, "runtime.onInstalled fired with the correct reason");
} else {
equal(details.fired, onInstalledFired, "runtime.onInstalled should not have fired");
}
extension.sendMessage("did-on-startup-fire");
let fired = yield extension.awaitMessage("on-startup-fired");
equal(fired, onStartupFired, `Expected runtime.onStartup to ${onStartupFired ? "" : "not "} fire`);
}
add_task(function* test_should_fire_on_addon_update() {
const EXTENSION_ID = "test_runtime_on_installed_addon_update@tests.mozilla.org";
const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
// The test extension uses an insecure update url.
Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
const testServer = createHttpServer();
const port = testServer.identity.primaryPort;
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
"version": "1.0",
"applications": {
"gecko": {
"id": EXTENSION_ID,
"update_url": `http://localhost:${port}/test_update.json`,
},
},
},
background,
});
testServer.registerPathHandler("/test_update.json", (request, response) => {
response.write(`{
"addons": {
"${EXTENSION_ID}": {
"updates": [
{
"version": "2.0",
"update_link": "http://localhost:${port}/addons/test_runtime_on_installed-2.0.xpi"
}
]
}
}
}`);
});
let webExtensionFile = createTempWebExtensionFile({
manifest: {
version: "2.0",
applications: {
gecko: {
id: EXTENSION_ID,
},
},
},
background,
});
testServer.registerFile("/addons/test_runtime_on_installed-2.0.xpi", webExtensionFile);
yield promiseStartupManager();
yield extension.startup();
yield expectEvents(extension, {
onStartupFired: false,
onInstalledFired: true,
onInstalledReason: "install",
});
let addon = yield promiseAddonByID(EXTENSION_ID);
equal(addon.version, "1.0", "The installed addon has the correct version");
let update = yield promiseFindAddonUpdates(addon);
let install = update.updateAvailable;
let promiseInstalled = promiseAddonEvent("onInstalled");
yield promiseCompleteAllInstalls([install]);
yield extension.awaitMessage("reloading");
let startupPromise = awaitEvent("ready");
let [updated_addon] = yield promiseInstalled;
equal(updated_addon.version, "2.0", "The updated addon has the correct version");
extension.extension = yield startupPromise;
extension.attachListeners();
yield expectEvents(extension, {
onStartupFired: false,
onInstalledFired: true,
onInstalledReason: "update",
});
yield extension.unload();
yield updated_addon.uninstall();
yield promiseShutdownManager();
});
add_task(function* test_should_fire_on_browser_update() {
const EXTENSION_ID = "test_runtime_on_installed_browser_update@tests.mozilla.org";
yield promiseStartupManager();
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
"version": "1.0",
"applications": {
"gecko": {
"id": EXTENSION_ID,
},
},
},
background,
});
yield extension.startup();
yield expectEvents(extension, {
onStartupFired: false,
onInstalledFired: true,
onInstalledReason: "install",
});
let startupPromise = awaitEvent("ready");
yield promiseRestartManager("1");
extension.extension = yield startupPromise;
extension.attachListeners();
yield expectEvents(extension, {
onStartupFired: true,
onInstalledFired: false,
});
// Update the browser.
startupPromise = awaitEvent("ready");
yield promiseRestartManager("2");
extension.extension = yield startupPromise;
extension.attachListeners();
yield expectEvents(extension, {
onStartupFired: true,
onInstalledFired: true,
onInstalledReason: "browser_update",
});
// Restart the browser.
startupPromise = awaitEvent("ready");
yield promiseRestartManager("2");
extension.extension = yield startupPromise;
extension.attachListeners();
yield expectEvents(extension, {
onStartupFired: true,
onInstalledFired: false,
});
// Update the browser again.
startupPromise = awaitEvent("ready");
yield promiseRestartManager("3");
extension.extension = yield startupPromise;
extension.attachListeners();
yield expectEvents(extension, {
onStartupFired: true,
onInstalledFired: true,
onInstalledReason: "browser_update",
});
yield extension.unload();
yield promiseShutdownManager();
});
add_task(function* test_should_not_fire_on_reload() {
const EXTENSION_ID = "test_runtime_on_installed_reload@tests.mozilla.org";
yield promiseStartupManager();
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
"version": "1.0",
"applications": {
"gecko": {
"id": EXTENSION_ID,
},
},
},
background,
});
yield extension.startup();
yield expectEvents(extension, {
onStartupFired: false,
onInstalledFired: true,
onInstalledReason: "install",
});
let startupPromise = awaitEvent("ready");
extension.sendMessage("reload-extension");
extension.extension = yield startupPromise;
extension.attachListeners();
yield expectEvents(extension, {
onStartupFired: false,
onInstalledFired: false,
});
yield extension.unload();
yield promiseShutdownManager();
});
add_task(function* test_should_not_fire_on_restart() {
const EXTENSION_ID = "test_runtime_on_installed_restart@tests.mozilla.org";
yield promiseStartupManager();
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
"version": "1.0",
"applications": {
"gecko": {
"id": EXTENSION_ID,
},
},
},
background,
});
yield extension.startup();
yield expectEvents(extension, {
onStartupFired: false,
onInstalledFired: true,
onInstalledReason: "install",
});
let addon = yield promiseAddonByID(EXTENSION_ID);
addon.userDisabled = true;
let startupPromise = awaitEvent("ready");
addon.userDisabled = false;
extension.extension = yield startupPromise;
extension.attachListeners();
yield expectEvents(extension, {
onStartupFired: false,
onInstalledFired: false,
});
yield extension.markUnloaded();
yield promiseShutdownManager();
});

View file

@ -0,0 +1,79 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* tabsSendMessageReply() {
function background() {
browser.runtime.onMessage.addListener((msg, sender, respond) => {
if (msg == "respond-now") {
respond(msg);
} else if (msg == "respond-soon") {
setTimeout(() => { respond(msg); }, 0);
return true;
} else if (msg == "respond-promise") {
return Promise.resolve(msg);
} else if (msg == "respond-never") {
return;
} else if (msg == "respond-error") {
return Promise.reject(new Error(msg));
} else if (msg == "throw-error") {
throw new Error(msg);
}
});
browser.runtime.onMessage.addListener((msg, sender, respond) => {
if (msg == "respond-now") {
respond("hello");
} else if (msg == "respond-now-2") {
respond(msg);
}
});
let childFrame = document.createElement("iframe");
childFrame.src = "extensionpage.html";
document.body.appendChild(childFrame);
}
function senderScript() {
Promise.all([
browser.runtime.sendMessage("respond-now"),
browser.runtime.sendMessage("respond-now-2"),
new Promise(resolve => browser.runtime.sendMessage("respond-soon", resolve)),
browser.runtime.sendMessage("respond-promise"),
browser.runtime.sendMessage("respond-never"),
new Promise(resolve => {
browser.runtime.sendMessage("respond-never", response => { resolve(response); });
}),
browser.runtime.sendMessage("respond-error").catch(error => Promise.resolve({error})),
browser.runtime.sendMessage("throw-error").catch(error => Promise.resolve({error})),
]).then(([respondNow, respondNow2, respondSoon, respondPromise, respondNever, respondNever2, respondError, throwError]) => {
browser.test.assertEq("respond-now", respondNow, "Got the expected immediate response");
browser.test.assertEq("respond-now-2", respondNow2, "Got the expected immediate response from the second listener");
browser.test.assertEq("respond-soon", respondSoon, "Got the expected delayed response");
browser.test.assertEq("respond-promise", respondPromise, "Got the expected promise response");
browser.test.assertEq(undefined, respondNever, "Got the expected no-response resolution");
browser.test.assertEq(undefined, respondNever2, "Got the expected no-response resolution");
browser.test.assertEq("respond-error", respondError.error.message, "Got the expected error response");
browser.test.assertEq("throw-error", throwError.error.message, "Got the expected thrown error response");
browser.test.notifyPass("sendMessage");
}).catch(e => {
browser.test.fail(`Error: ${e} :: ${e.stack}`);
browser.test.notifyFail("sendMessage");
});
}
let extension = ExtensionTestUtils.loadExtension({
background,
files: {
"senderScript.js": senderScript,
"extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`,
},
});
yield extension.startup();
yield extension.awaitFinish("sendMessage");
yield extension.unload();
});

View file

@ -0,0 +1,59 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_sendMessage_error() {
async function background() {
let circ = {};
circ.circ = circ;
let testCases = [
// [arguments, expected error string],
[[], "runtime.sendMessage's message argument is missing"],
[[null, null, null, null], "runtime.sendMessage's last argument is not a function"],
[[null, null, 1], "runtime.sendMessage's options argument is invalid"],
[[1, null, null], "runtime.sendMessage's extensionId argument is invalid"],
[[null, null, null, null, null], "runtime.sendMessage received too many arguments"],
// Even when the parameters are accepted, we still expect an error
// because there is no onMessage listener.
[[null, null, null], "Could not establish connection. Receiving end does not exist."],
// Structural cloning doesn't work with DOM but we fall back
// JSON serialization, so we don't expect another error.
[[null, location, null], "Could not establish connection. Receiving end does not exist."],
// Structured cloning supports cyclic self-references.
[[null, [circ, location], null], "cyclic object value"],
// JSON serialization does not support cyclic references.
[[null, circ, null], "Could not establish connection. Receiving end does not exist."],
// (the last two tests shows whether sendMessage is implemented as structured cloning).
];
// Repeat all tests with the undefined value instead of null.
for (let [args, expectedError] of testCases.slice()) {
args = args.map(arg => arg === null ? undefined : arg);
testCases.push([args, expectedError]);
}
for (let [args, expectedError] of testCases) {
let description = `runtime.sendMessage(${args.map(String).join(", ")})`;
await browser.test.assertRejects(
browser.runtime.sendMessage(...args),
expectedError,
`expected error message for ${description}`);
}
browser.test.notifyPass("sendMessage parameter validation");
}
let extensionData = {
background,
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitFinish("sendMessage parameter validation");
yield extension.unload();
});

View file

@ -0,0 +1,54 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_sendMessage_without_listener() {
async function background() {
await browser.test.assertRejects(
browser.runtime.sendMessage("msg"),
"Could not establish connection. Receiving end does not exist.",
"sendMessage callback was invoked");
browser.test.notifyPass("sendMessage callback was invoked");
}
let extensionData = {
background,
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitFinish("sendMessage callback was invoked");
yield extension.unload();
});
add_task(function* test_chrome_sendMessage_without_listener() {
function background() {
/* globals chrome */
browser.test.assertEq(null, chrome.runtime.lastError, "no lastError before call");
let retval = chrome.runtime.sendMessage("msg");
browser.test.assertEq(null, chrome.runtime.lastError, "no lastError after call");
browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage without callback");
let isAsyncCall = false;
retval = chrome.runtime.sendMessage("msg", reply => {
browser.test.assertEq(undefined, reply, "no reply");
browser.test.assertTrue(isAsyncCall, "chrome.runtime.sendMessage's callback must be called asynchronously");
browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage with callback");
browser.test.assertEq("Could not establish connection. Receiving end does not exist.", chrome.runtime.lastError.message);
browser.test.notifyPass("finished chrome.runtime.sendMessage");
});
isAsyncCall = true;
}
let extensionData = {
background,
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitFinish("finished chrome.runtime.sendMessage");
yield extension.unload();
});

View file

@ -0,0 +1,51 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_sendMessage_to_self_should_not_trigger_onMessage() {
async function background() {
browser.runtime.onMessage.addListener(msg => {
browser.test.assertEq("msg from child", msg);
browser.test.notifyPass("sendMessage did not call same-frame onMessage");
});
browser.test.onMessage.addListener(msg => {
browser.test.assertEq("sendMessage with a listener in another frame", msg);
browser.runtime.sendMessage("should only reach another frame");
});
await browser.test.assertRejects(
browser.runtime.sendMessage("should not trigger same-frame onMessage"),
"Could not establish connection. Receiving end does not exist.");
let anotherFrame = document.createElement("iframe");
anotherFrame.src = browser.extension.getURL("extensionpage.html");
document.body.appendChild(anotherFrame);
}
function lastScript() {
browser.runtime.onMessage.addListener(msg => {
browser.test.assertEq("should only reach another frame", msg);
browser.runtime.sendMessage("msg from child");
});
browser.test.sendMessage("sendMessage callback called");
}
let extensionData = {
background,
files: {
"lastScript.js": lastScript,
"extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="lastScript.js"></script>`,
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitMessage("sendMessage callback called");
extension.sendMessage("sendMessage with a listener in another frame");
yield extension.awaitFinish("sendMessage did not call same-frame onMessage");
yield extension.unload();
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,147 @@
"use strict";
Components.utils.import("resource://gre/modules/Schemas.jsm");
let schemaJson = [
{
namespace: "noAllowedContexts",
properties: {
prop1: {type: "object"},
prop2: {type: "object", allowedContexts: ["test_zero", "test_one"]},
prop3: {type: "number", value: 1},
prop4: {type: "number", value: 1, allowedContexts: ["numeric_one"]},
},
},
{
namespace: "defaultContexts",
defaultContexts: ["test_two"],
properties: {
prop1: {type: "object"},
prop2: {type: "object", allowedContexts: ["test_three"]},
prop3: {type: "number", value: 1},
prop4: {type: "number", value: 1, allowedContexts: ["numeric_two"]},
},
},
{
namespace: "withAllowedContexts",
allowedContexts: ["test_four"],
properties: {
prop1: {type: "object"},
prop2: {type: "object", allowedContexts: ["test_five"]},
prop3: {type: "number", value: 1},
prop4: {type: "number", value: 1, allowedContexts: ["numeric_three"]},
},
},
{
namespace: "withAllowedContextsAndDefault",
allowedContexts: ["test_six"],
defaultContexts: ["test_seven"],
properties: {
prop1: {type: "object"},
prop2: {type: "object", allowedContexts: ["test_eight"]},
prop3: {type: "number", value: 1},
prop4: {type: "number", value: 1, allowedContexts: ["numeric_four"]},
},
},
{
namespace: "with_submodule",
defaultContexts: ["test_nine"],
types: [{
id: "subtype",
type: "object",
functions: [{
name: "noAllowedContexts",
type: "function",
parameters: [],
}, {
name: "allowedContexts",
allowedContexts: ["test_ten"],
type: "function",
parameters: [],
}],
}],
properties: {
prop1: {$ref: "subtype"},
prop2: {$ref: "subtype", allowedContexts: ["test_eleven"]},
},
},
];
add_task(function* testRestrictions() {
let url = "data:," + JSON.stringify(schemaJson);
yield Schemas.load(url);
let results = {};
let localWrapper = {
shouldInject(ns, name, allowedContexts) {
name = name === null ? ns : ns + "." + name;
results[name] = allowedContexts.join(",");
return true;
},
getImplementation() {
// The actual implementation is not significant for this test.
// Let's take this opportunity to see if schema generation is free of
// exceptions even when somehow getImplementation does not return an
// implementation.
},
};
let root = {};
Schemas.inject(root, localWrapper);
function verify(path, expected) {
let obj = root;
for (let thing of path.split(".")) {
try {
obj = obj[thing];
} catch (e) {
// Blech.
}
}
let result = results[path];
equal(result, expected);
}
verify("noAllowedContexts", "");
verify("noAllowedContexts.prop1", "");
verify("noAllowedContexts.prop2", "test_zero,test_one");
verify("noAllowedContexts.prop3", "");
verify("noAllowedContexts.prop4", "numeric_one");
verify("defaultContexts", "");
verify("defaultContexts.prop1", "test_two");
verify("defaultContexts.prop2", "test_three");
verify("defaultContexts.prop3", "test_two");
verify("defaultContexts.prop4", "numeric_two");
verify("withAllowedContexts", "test_four");
verify("withAllowedContexts.prop1", "");
verify("withAllowedContexts.prop2", "test_five");
verify("withAllowedContexts.prop3", "");
verify("withAllowedContexts.prop4", "numeric_three");
verify("withAllowedContextsAndDefault", "test_six");
verify("withAllowedContextsAndDefault.prop1", "test_seven");
verify("withAllowedContextsAndDefault.prop2", "test_eight");
verify("withAllowedContextsAndDefault.prop3", "test_seven");
verify("withAllowedContextsAndDefault.prop4", "numeric_four");
verify("with_submodule", "");
verify("with_submodule.prop1", "test_nine");
verify("with_submodule.prop1.noAllowedContexts", "test_nine");
verify("with_submodule.prop1.allowedContexts", "test_ten");
verify("with_submodule.prop2", "test_eleven");
// Note: test_nine inherits allowed contexts from the namespace, not from
// submodule. There is no "defaultContexts" for submodule types to not
// complicate things.
verify("with_submodule.prop1.noAllowedContexts", "test_nine");
verify("with_submodule.prop1.allowedContexts", "test_ten");
// This is a constant, so it does not matter that getImplementation does not
// return an implementation since the API injector should take care of it.
equal(root.noAllowedContexts.prop3, 1);
Assert.throws(() => root.noAllowedContexts.prop1,
/undefined/,
"Should throw when the implementation is absent.");
});

View file

@ -0,0 +1,102 @@
"use strict";
Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
Components.utils.import("resource://gre/modules/Schemas.jsm");
let {
BaseContext,
SchemaAPIManager,
} = ExtensionCommon;
let nestedNamespaceJson = [
{
"namespace": "backgroundAPI.testnamespace",
"functions": [
{
"name": "create",
"type": "function",
"parameters": [
{
"name": "title",
"type": "string",
},
],
"returns": {
"type": "string",
},
},
],
},
{
"namespace": "noBackgroundAPI.testnamespace",
"functions": [
{
"name": "create",
"type": "function",
"parameters": [
{
"name": "title",
"type": "string",
},
],
},
],
},
];
let global = this;
class StubContext extends BaseContext {
constructor() {
let fakeExtension = {id: "test@web.extension"};
super("addon_child", fakeExtension);
this.sandbox = Cu.Sandbox(global);
this.viewType = "background";
}
get cloneScope() {
return this.sandbox;
}
}
add_task(function* testSchemaAPIInjection() {
let url = "data:," + JSON.stringify(nestedNamespaceJson);
// Load the schema of the fake APIs.
yield Schemas.load(url);
let apiManager = new SchemaAPIManager("addon");
// Register an API that will skip the background page.
apiManager.registerSchemaAPI("noBackgroundAPI.testnamespace", "addon_child", context => {
// This API should not be available in this context, return null so that
// the schema wrapper is removed as well.
return null;
});
// Register an API that will skip any but the background page.
apiManager.registerSchemaAPI("backgroundAPI.testnamespace", "addon_child", context => {
if (context.viewType === "background") {
return {
backgroundAPI: {
testnamespace: {
create(title) {
return title;
},
},
},
};
}
// This API should not be available in this context, return null so that
// the schema wrapper is removed as well.
return null;
});
let context = new StubContext();
let browserObj = {};
apiManager.generateAPIs(context, browserObj);
do_check_eq(browserObj.noBackgroundAPI, undefined);
const res = browserObj.backgroundAPI.testnamespace.create("param-value");
do_check_eq(res, "param-value");
});

View file

@ -0,0 +1,232 @@
"use strict";
Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
Components.utils.import("resource://gre/modules/Schemas.jsm");
let {BaseContext, LocalAPIImplementation} = ExtensionCommon;
let schemaJson = [
{
namespace: "testnamespace",
functions: [{
name: "one_required",
type: "function",
parameters: [{
name: "first",
type: "function",
parameters: [],
}],
}, {
name: "one_optional",
type: "function",
parameters: [{
name: "first",
type: "function",
parameters: [],
optional: true,
}],
}, {
name: "async_required",
type: "function",
async: "first",
parameters: [{
name: "first",
type: "function",
parameters: [],
}],
}, {
name: "async_optional",
type: "function",
async: "first",
parameters: [{
name: "first",
type: "function",
parameters: [],
optional: true,
}],
}],
},
];
const global = this;
class StubContext extends BaseContext {
constructor() {
let fakeExtension = {id: "test@web.extension"};
super("testEnv", fakeExtension);
this.sandbox = Cu.Sandbox(global);
}
get cloneScope() {
return this.sandbox;
}
get principal() {
return Cu.getObjectPrincipal(this.sandbox);
}
}
let context;
function generateAPIs(extraWrapper, apiObj) {
context = new StubContext();
let localWrapper = {
shouldInject() {
return true;
},
getImplementation(namespace, name) {
return new LocalAPIImplementation(apiObj, name, context);
},
};
Object.assign(localWrapper, extraWrapper);
let root = {};
Schemas.inject(root, localWrapper);
return root.testnamespace;
}
add_task(function* testParameterValidation() {
yield Schemas.load("data:," + JSON.stringify(schemaJson));
let testnamespace;
function assertThrows(name, ...args) {
Assert.throws(() => testnamespace[name](...args),
/Incorrect argument types/,
`Expected testnamespace.${name}(${args.map(String).join(", ")}) to throw.`);
}
function assertNoThrows(name, ...args) {
try {
testnamespace[name](...args);
} catch (e) {
do_print(`testnamespace.${name}(${args.map(String).join(", ")}) unexpectedly threw.`);
throw new Error(e);
}
}
let cb = () => {};
for (let isChromeCompat of [true, false]) {
do_print(`Testing API validation with isChromeCompat=${isChromeCompat}`);
testnamespace = generateAPIs({
isChromeCompat,
}, {
one_required() {},
one_optional() {},
async_required() {},
async_optional() {},
});
assertThrows("one_required");
assertThrows("one_required", null);
assertNoThrows("one_required", cb);
assertThrows("one_required", cb, null);
assertThrows("one_required", cb, cb);
assertNoThrows("one_optional");
assertNoThrows("one_optional", null);
assertNoThrows("one_optional", cb);
assertThrows("one_optional", cb, null);
assertThrows("one_optional", cb, cb);
// Schema-based validation happens before an async method is called, so
// errors should be thrown synchronously.
// The parameter was declared as required, but there was also an "async"
// attribute with the same value as the parameter name, so the callback
// parameter is actually optional.
assertNoThrows("async_required");
assertNoThrows("async_required", null);
assertNoThrows("async_required", cb);
assertThrows("async_required", cb, null);
assertThrows("async_required", cb, cb);
assertNoThrows("async_optional");
assertNoThrows("async_optional", null);
assertNoThrows("async_optional", cb);
assertThrows("async_optional", cb, null);
assertThrows("async_optional", cb, cb);
}
});
add_task(function* testAsyncResults() {
yield Schemas.load("data:," + JSON.stringify(schemaJson));
function* runWithCallback(func) {
do_print(`Calling testnamespace.${func.name}, expecting callback with result`);
return yield new Promise(resolve => {
let result = "uninitialized value";
let returnValue = func(reply => {
result = reply;
resolve(result);
});
// When a callback is given, the return value must be missing.
do_check_eq(returnValue, undefined);
// Callback must be called asynchronously.
do_check_eq(result, "uninitialized value");
});
}
function* runFailCallback(func) {
do_print(`Calling testnamespace.${func.name}, expecting callback with error`);
return yield new Promise(resolve => {
func(reply => {
do_check_eq(reply, undefined);
resolve(context.lastError.message); // eslint-disable-line no-undef
});
});
}
for (let isChromeCompat of [true, false]) {
do_print(`Testing API invocation with isChromeCompat=${isChromeCompat}`);
let testnamespace = generateAPIs({
isChromeCompat,
}, {
async_required(cb) {
do_check_eq(cb, undefined);
return Promise.resolve(1);
},
async_optional(cb) {
do_check_eq(cb, undefined);
return Promise.resolve(2);
},
});
if (!isChromeCompat) { // No promises for chrome.
do_print("testnamespace.async_required should be a Promise");
let promise = testnamespace.async_required();
do_check_true(promise instanceof context.cloneScope.Promise);
do_check_eq(yield promise, 1);
do_print("testnamespace.async_optional should be a Promise");
promise = testnamespace.async_optional();
do_check_true(promise instanceof context.cloneScope.Promise);
do_check_eq(yield promise, 2);
}
do_check_eq(yield* runWithCallback(testnamespace.async_required), 1);
do_check_eq(yield* runWithCallback(testnamespace.async_optional), 2);
let otherSandbox = Cu.Sandbox(null, {});
let errorFactories = [
msg => { throw new context.cloneScope.Error(msg); },
msg => context.cloneScope.Promise.reject({message: msg}),
msg => Cu.evalInSandbox(`throw new Error("${msg}")`, otherSandbox),
msg => Cu.evalInSandbox(`Promise.reject({message: "${msg}"})`, otherSandbox),
];
for (let makeError of errorFactories) {
do_print(`Testing callback/promise with error caused by: ${makeError}`);
testnamespace = generateAPIs({
isChromeCompat,
}, {
async_required() { return makeError("ONE"); },
async_optional() { return makeError("TWO"); },
});
if (!isChromeCompat) { // No promises for chrome.
yield Assert.rejects(testnamespace.async_required(), /ONE/,
"should reject testnamespace.async_required()").catch(() => {});
yield Assert.rejects(testnamespace.async_optional(), /TWO/,
"should reject testnamespace.async_optional()").catch(() => {});
}
do_check_eq(yield* runFailCallback(testnamespace.async_required), "ONE");
do_check_eq(yield* runFailCallback(testnamespace.async_optional), "TWO");
}
}
});

View file

@ -0,0 +1,69 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_simple() {
let extensionData = {
manifest: {
"name": "Simple extension test",
"version": "1.0",
"manifest_version": 2,
"description": "",
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.unload();
});
add_task(function* test_background() {
function background() {
browser.test.log("running background script");
browser.test.onMessage.addListener((x, y) => {
browser.test.assertEq(x, 10, "x is 10");
browser.test.assertEq(y, 20, "y is 20");
browser.test.notifyPass("background test passed");
});
browser.test.sendMessage("running", 1);
}
let extensionData = {
background,
manifest: {
"name": "Simple extension test",
"version": "1.0",
"manifest_version": 2,
"description": "",
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
equal(x, 1, "got correct value from extension");
extension.sendMessage(10, 20);
yield extension.awaitFinish();
yield extension.unload();
});
add_task(function* test_extensionTypes() {
let extensionData = {
background: function() {
browser.test.assertEq(typeof browser.extensionTypes, "object", "browser.extensionTypes exists");
browser.test.assertEq(typeof browser.extensionTypes.RunAt, "object", "browser.extensionTypes.RunAt exists");
browser.test.notifyPass("extentionTypes test passed");
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitFinish();
yield extension.unload();
});

View file

@ -0,0 +1,334 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled";
Cu.import("resource://gre/modules/Preferences.jsm");
/**
* Utility function to ensure that all supported APIs for getting are
* tested.
*
* @param {string} areaName
* either "local" or "sync" according to what we want to test
* @param {string} prop
* "key" to look up using the storage API
* @param {Object} value
* "value" to compare against
*/
async function checkGetImpl(areaName, prop, value) {
let storage = browser.storage[areaName];
let data = await storage.get(null);
browser.test.assertEq(value, data[prop], `null getter worked for ${prop} in ${areaName}`);
data = await storage.get(prop);
browser.test.assertEq(value, data[prop], `string getter worked for ${prop} in ${areaName}`);
data = await storage.get([prop]);
browser.test.assertEq(value, data[prop], `array getter worked for ${prop} in ${areaName}`);
data = await storage.get({[prop]: undefined});
browser.test.assertEq(value, data[prop], `object getter worked for ${prop} in ${areaName}`);
}
add_task(function* test_local_cache_invalidation() {
function background(checkGet) {
browser.test.onMessage.addListener(async msg => {
if (msg === "set-initial") {
await browser.storage.local.set({"test-prop1": "value1", "test-prop2": "value2"});
browser.test.sendMessage("set-initial-done");
} else if (msg === "check") {
await checkGet("local", "test-prop1", "value1");
await checkGet("local", "test-prop2", "value2");
browser.test.sendMessage("check-done");
}
});
browser.test.sendMessage("ready");
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["storage"],
},
background: `(${background})(${checkGetImpl})`,
});
yield extension.startup();
yield extension.awaitMessage("ready");
extension.sendMessage("set-initial");
yield extension.awaitMessage("set-initial-done");
Services.obs.notifyObservers(null, "extension-invalidate-storage-cache", "");
extension.sendMessage("check");
yield extension.awaitMessage("check-done");
yield extension.unload();
});
add_task(function* test_config_flag_needed() {
function background() {
let promises = [];
let apiTests = [
{method: "get", args: ["foo"]},
{method: "set", args: [{foo: "bar"}]},
{method: "remove", args: ["foo"]},
{method: "clear", args: []},
];
apiTests.forEach(testDef => {
promises.push(browser.test.assertRejects(
browser.storage.sync[testDef.method](...testDef.args),
"Please set webextensions.storage.sync.enabled to true in about:config",
`storage.sync.${testDef.method} is behind a flag`));
});
Promise.all(promises).then(() => browser.test.notifyPass("flag needed"));
}
ok(!Preferences.get(STORAGE_SYNC_PREF));
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["storage"],
},
background: `(${background})(${checkGetImpl})`,
});
yield extension.startup();
yield extension.awaitFinish("flag needed");
yield extension.unload();
});
add_task(function* test_reloading_extensions_works() {
// Just some random extension ID that we can re-use
const extensionId = "my-extension-id@1";
function loadExtension() {
function background() {
browser.storage.sync.set({"a": "b"}).then(() => {
browser.test.notifyPass("set-works");
});
}
return ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["storage"],
},
background: `(${background})()`,
}, extensionId);
}
Preferences.set(STORAGE_SYNC_PREF, true);
let extension1 = loadExtension();
yield extension1.startup();
yield extension1.awaitFinish("set-works");
yield extension1.unload();
let extension2 = loadExtension();
yield extension2.startup();
yield extension2.awaitFinish("set-works");
yield extension2.unload();
Preferences.reset(STORAGE_SYNC_PREF);
});
do_register_cleanup(() => {
Preferences.reset(STORAGE_SYNC_PREF);
});
add_task(function* test_backgroundScript() {
async function backgroundScript(checkGet) {
let globalChanges, gResolve;
function clearGlobalChanges() {
globalChanges = new Promise(resolve => { gResolve = resolve; });
}
clearGlobalChanges();
let expectedAreaName;
browser.storage.onChanged.addListener((changes, areaName) => {
browser.test.assertEq(expectedAreaName, areaName,
"Expected area name received by listener");
gResolve(changes);
});
async function checkChanges(areaName, changes, message) {
function checkSub(obj1, obj2) {
for (let prop in obj1) {
browser.test.assertTrue(obj1[prop] !== undefined,
`checkChanges ${areaName} ${prop} is missing (${message})`);
browser.test.assertTrue(obj2[prop] !== undefined,
`checkChanges ${areaName} ${prop} is missing (${message})`);
browser.test.assertEq(obj1[prop].oldValue, obj2[prop].oldValue,
`checkChanges ${areaName} ${prop} old (${message})`);
browser.test.assertEq(obj1[prop].newValue, obj2[prop].newValue,
`checkChanges ${areaName} ${prop} new (${message})`);
}
}
const recentChanges = await globalChanges;
checkSub(changes, recentChanges);
checkSub(recentChanges, changes);
clearGlobalChanges();
}
/* eslint-disable dot-notation */
async function runTests(areaName) {
expectedAreaName = areaName;
let storage = browser.storage[areaName];
// Set some data and then test getters.
try {
await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
await checkChanges(areaName,
{"test-prop1": {newValue: "value1"}, "test-prop2": {newValue: "value2"}},
"set (a)");
await checkGet(areaName, "test-prop1", "value1");
await checkGet(areaName, "test-prop2", "value2");
let data = await storage.get({"test-prop1": undefined, "test-prop2": undefined, "other": "default"});
browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (a)");
browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (a)");
browser.test.assertEq("default", data["other"], "other correct");
data = await storage.get(["test-prop1", "test-prop2", "other"]);
browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (b)");
browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (b)");
browser.test.assertFalse("other" in data, "other correct");
// Remove data in various ways.
await storage.remove("test-prop1");
await checkChanges(areaName, {"test-prop1": {oldValue: "value1"}}, "remove string");
data = await storage.get(["test-prop1", "test-prop2"]);
browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove string)");
browser.test.assertTrue("test-prop2" in data, "prop2 present (remove string)");
await storage.set({"test-prop1": "value1"});
await checkChanges(areaName, {"test-prop1": {newValue: "value1"}}, "set (c)");
data = await storage.get(["test-prop1", "test-prop2"]);
browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct (c)");
browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct (c)");
await storage.remove(["test-prop1", "test-prop2"]);
await checkChanges(areaName,
{"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}},
"remove array");
data = await storage.get(["test-prop1", "test-prop2"]);
browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove array)");
browser.test.assertFalse("test-prop2" in data, "prop2 absent (remove array)");
// test storage.clear
await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
// Make sure that set() handler happened before we clear the
// promise again.
await globalChanges;
clearGlobalChanges();
await storage.clear();
await checkChanges(areaName,
{"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}},
"clear");
data = await storage.get(["test-prop1", "test-prop2"]);
browser.test.assertFalse("test-prop1" in data, "prop1 absent (clear)");
browser.test.assertFalse("test-prop2" in data, "prop2 absent (clear)");
// Make sure we can store complex JSON data.
// known previous values
await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
// Make sure the set() handler landed.
await globalChanges;
clearGlobalChanges();
await storage.set({
"test-prop1": {
str: "hello",
bool: true,
null: null,
undef: undefined,
obj: {},
arr: [1, 2],
date: new Date(0),
regexp: /regexp/,
func: function func() {},
window,
},
});
await storage.set({"test-prop2": function func() {}});
const recentChanges = await globalChanges;
browser.test.assertEq("value1", recentChanges["test-prop1"].oldValue, "oldValue correct");
browser.test.assertEq("object", typeof(recentChanges["test-prop1"].newValue), "newValue is obj");
clearGlobalChanges();
data = await storage.get({"test-prop1": undefined, "test-prop2": undefined});
let obj = data["test-prop1"];
browser.test.assertEq("hello", obj.str, "string part correct");
browser.test.assertEq(true, obj.bool, "bool part correct");
browser.test.assertEq(null, obj.null, "null part correct");
browser.test.assertEq(undefined, obj.undef, "undefined part correct");
browser.test.assertEq(undefined, obj.func, "function part correct");
browser.test.assertEq(undefined, obj.window, "window part correct");
browser.test.assertEq("1970-01-01T00:00:00.000Z", obj.date, "date part correct");
browser.test.assertEq("/regexp/", obj.regexp, "regexp part correct");
browser.test.assertEq("object", typeof(obj.obj), "object part correct");
browser.test.assertTrue(Array.isArray(obj.arr), "array part present");
browser.test.assertEq(1, obj.arr[0], "arr[0] part correct");
browser.test.assertEq(2, obj.arr[1], "arr[1] part correct");
browser.test.assertEq(2, obj.arr.length, "arr.length part correct");
obj = data["test-prop2"];
browser.test.assertEq("[object Object]", {}.toString.call(obj), "function serialized as a plain object");
browser.test.assertEq(0, Object.keys(obj).length, "function serialized as an empty object");
} catch (e) {
browser.test.fail(`Error: ${e} :: ${e.stack}`);
browser.test.notifyFail("storage");
}
}
browser.test.onMessage.addListener(msg => {
let promise;
if (msg === "test-local") {
promise = runTests("local");
} else if (msg === "test-sync") {
promise = runTests("sync");
}
promise.then(() => browser.test.sendMessage("test-finished"));
});
browser.test.sendMessage("ready");
}
let extensionData = {
background: `(${backgroundScript})(${checkGetImpl})`,
manifest: {
permissions: ["storage"],
},
};
Preferences.set(STORAGE_SYNC_PREF, true);
let extension = ExtensionTestUtils.loadExtension(extensionData);
yield extension.startup();
yield extension.awaitMessage("ready");
extension.sendMessage("test-local");
yield extension.awaitMessage("test-finished");
extension.sendMessage("test-sync");
yield extension.awaitMessage("test-finished");
Preferences.reset(STORAGE_SYNC_PREF);
yield extension.unload();
});

View file

@ -0,0 +1,85 @@
"use strict";
Cu.import("resource://gre/modules/NewTabUtils.jsm");
function TestProvider(getLinksFn) {
this.getLinks = getLinksFn;
this._observers = new Set();
}
TestProvider.prototype = {
addObserver: function(observer) {
this._observers.add(observer);
},
notifyLinkChanged: function(link, index = -1, deleted = false) {
this._notifyObservers("onLinkChanged", link, index, deleted);
},
notifyManyLinksChanged: function() {
this._notifyObservers("onManyLinksChanged");
},
_notifyObservers: function(observerMethodName, ...args) {
args.unshift(this);
for (let obs of this._observers) {
if (obs[observerMethodName]) {
obs[observerMethodName].apply(NewTabUtils.links, args);
}
}
},
};
function makeLinks(links) {
// Important: To avoid test failures due to clock jitter on Windows XP, call
// Date.now() once here, not each time through the loop.
let frecency = 0;
let now = Date.now() * 1000;
let places = [];
links.map((link, i) => {
places.push({
url: link.url,
title: link.title,
lastVisitDate: now - i,
frecency: frecency++,
});
});
return places;
}
add_task(function* test_topSites() {
let expect = [{url: "http://example.com/", title: "site#-1"},
{url: "http://example0.com/", title: "site#0"},
{url: "http://example1.com/", title: "site#1"},
{url: "http://example2.com/", title: "site#2"},
{url: "http://example3.com/", title: "site#3"}];
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"permissions": [
"topSites",
],
},
background() {
browser.topSites.get(result => {
browser.test.sendMessage("done", result);
});
},
});
let expectedLinks = makeLinks(expect);
let provider = new TestProvider(done => done(expectedLinks));
NewTabUtils.initWithoutProviders();
NewTabUtils.links.addProvider(provider);
yield NewTabUtils.links.populateCache();
yield extension.startup();
let result = yield extension.awaitMessage("done");
Assert.deepEqual(expect, result, "got topSites");
yield extension.unload();
NewTabUtils.links.removeProvider(provider);
});

View file

@ -0,0 +1,55 @@
"use strict";
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
Cu.import("resource://gre/modules/Services.jsm");
function createWindowWithAddonId(addonId) {
let baseURI = Services.io.newURI("about:blank", null, null);
let originAttributes = {addonId};
let principal = Services.scriptSecurityManager
.createCodebasePrincipal(baseURI, originAttributes);
let chromeNav = Services.appShell.createWindowlessBrowser(true);
let interfaceRequestor = chromeNav.QueryInterface(Ci.nsIInterfaceRequestor);
let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
docShell.createAboutBlankContentViewer(principal);
return {chromeNav, window: docShell.contentViewer.DOMDocument.defaultView};
}
add_task(function* test_eventpages() {
const {getAPILevelForWindow, getAddonIdForWindow} = ExtensionManagement;
const {NO_PRIVILEGES, FULL_PRIVILEGES} = ExtensionManagement.API_LEVELS;
const FAKE_ADDON_ID = "fakeAddonId";
const OTHER_ADDON_ID = "otherFakeAddonId";
const EMPTY_ADDON_ID = "";
let fakeAddonId = createWindowWithAddonId(FAKE_ADDON_ID);
equal(getAddonIdForWindow(fakeAddonId.window), FAKE_ADDON_ID,
"the window has the expected addonId");
let apiLevel = getAPILevelForWindow(fakeAddonId.window, FAKE_ADDON_ID);
equal(apiLevel, FULL_PRIVILEGES,
"apiLevel for the window with the right addonId should be FULL_PRIVILEGES");
apiLevel = getAPILevelForWindow(fakeAddonId.window, OTHER_ADDON_ID);
equal(apiLevel, NO_PRIVILEGES,
"apiLevel for the window with a different addonId should be NO_PRIVILEGES");
fakeAddonId.chromeNav.close();
// NOTE: check that window with an empty addon Id (which are window that are
// not Extensions pages) always get no WebExtensions APIs.
let emptyAddonId = createWindowWithAddonId(EMPTY_ADDON_ID);
equal(getAddonIdForWindow(emptyAddonId.window), EMPTY_ADDON_ID,
"the window has the expected addonId");
apiLevel = getAPILevelForWindow(emptyAddonId.window, EMPTY_ADDON_ID);
equal(apiLevel, NO_PRIVILEGES,
"apiLevel for empty addonId should be NO_PRIVILEGES");
apiLevel = getAPILevelForWindow(emptyAddonId.window, OTHER_ADDON_ID);
equal(apiLevel, NO_PRIVILEGES,
"apiLevel for an 'empty addonId' window should be always NO_PRIVILEGES");
emptyAddonId.chromeNav.close();
});

View file

@ -0,0 +1,133 @@
"use strict";
const convService = Cc["@mozilla.org/streamConverters;1"]
.getService(Ci.nsIStreamConverterService);
const UUID = "72b61ee3-aceb-476c-be1b-0822b036c9f1";
const ADDON_ID = "test@web.extension";
const URI = NetUtil.newURI(`moz-extension://${UUID}/file.css`);
const FROM_TYPE = "application/vnd.mozilla.webext.unlocalized";
const TO_TYPE = "text/css";
function StringStream(string) {
let stream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stream.data = string;
return stream;
}
// Initialize the policy service with a stub localizer for our
// add-on ID.
add_task(function* init() {
const aps = Cc["@mozilla.org/addons/policy-service;1"]
.getService(Ci.nsIAddonPolicyService).wrappedJSObject;
let oldCallback = aps.setExtensionURIToAddonIdCallback(uri => {
if (uri.host == UUID) {
return ADDON_ID;
}
});
aps.setAddonLocalizeCallback(ADDON_ID, string => {
return string.replace(/__MSG_(.*?)__/g, "<localized-$1>");
});
do_register_cleanup(() => {
aps.setExtensionURIToAddonIdCallback(oldCallback);
aps.setAddonLocalizeCallback(ADDON_ID, null);
});
});
// Test that the synchronous converter works as expected with a
// simple string.
add_task(function* testSynchronousConvert() {
let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz");
let resultStream = convService.convert(stream, FROM_TYPE, TO_TYPE, URI);
let result = NetUtil.readInputStreamToString(resultStream, resultStream.available());
equal(result, "Foo <localized-xxx> bar <localized-yyy> baz");
});
// Test that the asynchronous converter works as expected with input
// split into multiple chunks, and a boundary in the middle of a
// replacement token.
add_task(function* testAsyncConvert() {
let listener;
let awaitResult = new Promise((resolve, reject) => {
listener = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener]),
onDataAvailable(request, context, inputStream, offset, count) {
this.resultParts.push(NetUtil.readInputStreamToString(inputStream, count));
},
onStartRequest() {
ok(!("resultParts" in this));
this.resultParts = [];
},
onStopRequest(request, context, statusCode) {
if (!Components.isSuccessCode(statusCode)) {
reject(new Error(statusCode));
}
resolve(this.resultParts.join("\n"));
},
};
});
let parts = ["Foo __MSG_x", "xx__ bar __MSG_yyy__ baz"];
let converter = convService.asyncConvertData(FROM_TYPE, TO_TYPE, listener, URI);
converter.onStartRequest(null, null);
for (let part of parts) {
converter.onDataAvailable(null, null, StringStream(part), 0, part.length);
}
converter.onStopRequest(null, null, Cr.NS_OK);
let result = yield awaitResult;
equal(result, "Foo <localized-xxx> bar <localized-yyy> baz");
});
// Test that attempting to initialize a converter with the URI of a
// nonexistent WebExtension fails.
add_task(function* testInvalidUUID() {
let uri = NetUtil.newURI("moz-extension://eb4f3be8-41c9-4970-aa6d-b84d1ecc02b2/file.css");
let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz");
// Assert.throws raise a TypeError exception when the expected param
// is an arrow function. (See Bug 1237961 for rationale)
let expectInvalidContextException = function(e) {
return e.result === Cr.NS_ERROR_INVALID_ARG && /Invalid context/.test(e);
};
Assert.throws(() => {
convService.convert(stream, FROM_TYPE, TO_TYPE, uri);
}, expectInvalidContextException);
Assert.throws(() => {
let listener = {QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener])};
convService.asyncConvertData(FROM_TYPE, TO_TYPE, listener, uri);
}, expectInvalidContextException);
});
// Test that an empty stream does not throw an NS_ERROR_ILLEGAL_VALUE.
add_task(function* testEmptyStream() {
let stream = StringStream("");
let resultStream = convService.convert(stream, FROM_TYPE, TO_TYPE, URI);
equal(resultStream.data, "");
});

View file

@ -0,0 +1,130 @@
"use strict";
Cu.import("resource://gre/modules/Extension.jsm");
/* globals ExtensionData */
const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
function* generateAddon(data) {
let id = uuidGenerator.generateUUID().number;
data = Object.assign({embedded: true}, data);
data.manifest = Object.assign({applications: {gecko: {id}}}, data.manifest);
let xpi = Extension.generateXPI(data);
do_register_cleanup(() => {
Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
xpi.remove(false);
});
let fileURI = Services.io.newFileURI(xpi);
let jarURI = NetUtil.newURI(`jar:${fileURI.spec}!/webextension/`);
let extension = new ExtensionData(jarURI);
yield extension.readManifest();
return extension;
}
add_task(function* testMissingDefaultLocale() {
let extension = yield generateAddon({
"files": {
"_locales/en_US/messages.json": {},
},
});
equal(extension.errors.length, 0, "No errors reported");
yield extension.initAllLocales();
equal(extension.errors.length, 1, "One error reported");
do_print(`Got error: ${extension.errors[0]}`);
ok(extension.errors[0].includes('"default_locale" property is required'),
"Got missing default_locale error");
});
add_task(function* testInvalidDefaultLocale() {
let extension = yield generateAddon({
"manifest": {
"default_locale": "en",
},
"files": {
"_locales/en_US/messages.json": {},
},
});
equal(extension.errors.length, 1, "One error reported");
do_print(`Got error: ${extension.errors[0]}`);
ok(extension.errors[0].includes("Loading locale file _locales/en/messages.json"),
"Got invalid default_locale error");
yield extension.initAllLocales();
equal(extension.errors.length, 2, "Two errors reported");
do_print(`Got error: ${extension.errors[1]}`);
ok(extension.errors[1].includes('"default_locale" property must correspond'),
"Got invalid default_locale error");
});
add_task(function* testUnexpectedDefaultLocale() {
let extension = yield generateAddon({
"manifest": {
"default_locale": "en_US",
},
});
equal(extension.errors.length, 1, "One error reported");
do_print(`Got error: ${extension.errors[0]}`);
ok(extension.errors[0].includes("Loading locale file _locales/en-US/messages.json"),
"Got invalid default_locale error");
yield extension.initAllLocales();
equal(extension.errors.length, 2, "One error reported");
do_print(`Got error: ${extension.errors[1]}`);
ok(extension.errors[1].includes('"default_locale" property must correspond'),
"Got unexpected default_locale error");
});
add_task(function* testInvalidSyntax() {
let extension = yield generateAddon({
"manifest": {
"default_locale": "en_US",
},
"files": {
"_locales/en_US/messages.json": '{foo: {message: "bar", description: "baz"}}',
},
});
equal(extension.errors.length, 1, "No errors reported");
do_print(`Got error: ${extension.errors[0]}`);
ok(extension.errors[0].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"),
"Got syntax error");
yield extension.initAllLocales();
equal(extension.errors.length, 2, "One error reported");
do_print(`Got error: ${extension.errors[1]}`);
ok(extension.errors[1].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"),
"Got syntax error");
});

View file

@ -0,0 +1,302 @@
"use strict";
/* global OS, HostManifestManager, NativeApp */
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Schemas.jsm");
Cu.import("resource://gre/modules/Services.jsm");
const {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
Cu.import("resource://gre/modules/NativeMessaging.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
let registry = null;
if (AppConstants.platform == "win") {
Cu.import("resource://testing-common/MockRegistry.jsm");
registry = new MockRegistry();
do_register_cleanup(() => {
registry.shutdown();
});
}
const REGPATH = "Software\\Mozilla\\NativeMessagingHosts";
const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
let dir = FileUtils.getDir("TmpD", ["NativeMessaging"]);
dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
let userDir = dir.clone();
userDir.append("user");
userDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
let globalDir = dir.clone();
globalDir.append("global");
globalDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
let dirProvider = {
getFile(property) {
if (property == "XREUserNativeMessaging") {
return userDir.clone();
} else if (property == "XRESysNativeMessaging") {
return globalDir.clone();
}
return null;
},
};
Services.dirsvc.registerProvider(dirProvider);
do_register_cleanup(() => {
Services.dirsvc.unregisterProvider(dirProvider);
dir.remove(true);
});
function writeManifest(path, manifest) {
if (typeof manifest != "string") {
manifest = JSON.stringify(manifest);
}
return OS.File.writeAtomic(path, manifest);
}
let PYTHON;
add_task(function* setup() {
yield Schemas.load(BASE_SCHEMA);
PYTHON = yield Subprocess.pathSearch("python2.7");
if (PYTHON == null) {
PYTHON = yield Subprocess.pathSearch("python");
}
notEqual(PYTHON, null, "Found a suitable python interpreter");
});
let global = this;
// Test of HostManifestManager.lookupApplication() begin here...
let context = {
url: null,
jsonStringify(...args) { return JSON.stringify(...args); },
cloneScope: global,
logError() {},
preprocessors: {},
callOnClose: () => {},
forgetOnClose: () => {},
};
class MockContext extends ExtensionCommon.BaseContext {
constructor(extensionId) {
let fakeExtension = {id: extensionId};
super("testEnv", fakeExtension);
this.sandbox = Cu.Sandbox(global);
}
get cloneScope() {
return global;
}
get principal() {
return Cu.getObjectPrincipal(this.sandbox);
}
}
let templateManifest = {
name: "test",
description: "this is only a test",
path: "/bin/cat",
type: "stdio",
allowed_extensions: ["extension@tests.mozilla.org"],
};
add_task(function* test_nonexistent_manifest() {
let result = yield HostManifestManager.lookupApplication("test", context);
equal(result, null, "lookupApplication returns null for non-existent application");
});
const USER_TEST_JSON = OS.Path.join(userDir.path, "test.json");
add_task(function* test_good_manifest() {
yield writeManifest(USER_TEST_JSON, templateManifest);
if (registry) {
registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
`${REGPATH}\\test`, "", USER_TEST_JSON);
}
let result = yield HostManifestManager.lookupApplication("test", context);
notEqual(result, null, "lookupApplication finds a good manifest");
equal(result.path, USER_TEST_JSON, "lookupApplication returns the correct path");
deepEqual(result.manifest, templateManifest, "lookupApplication returns the manifest contents");
});
add_task(function* test_invalid_json() {
yield writeManifest(USER_TEST_JSON, "this is not valid json");
let result = yield HostManifestManager.lookupApplication("test", context);
equal(result, null, "lookupApplication ignores bad json");
});
add_task(function* test_invalid_name() {
let manifest = Object.assign({}, templateManifest);
manifest.name = "../test";
yield writeManifest(USER_TEST_JSON, manifest);
let result = yield HostManifestManager.lookupApplication("test", context);
equal(result, null, "lookupApplication ignores an invalid name");
});
add_task(function* test_name_mismatch() {
let manifest = Object.assign({}, templateManifest);
manifest.name = "not test";
yield writeManifest(USER_TEST_JSON, manifest);
let result = yield HostManifestManager.lookupApplication("test", context);
let what = (AppConstants.platform == "win") ? "registry key" : "json filename";
equal(result, null, `lookupApplication ignores mistmatch between ${what} and name property`);
});
add_task(function* test_missing_props() {
const PROPS = [
"name",
"description",
"path",
"type",
"allowed_extensions",
];
for (let prop of PROPS) {
let manifest = Object.assign({}, templateManifest);
delete manifest[prop];
yield writeManifest(USER_TEST_JSON, manifest);
let result = yield HostManifestManager.lookupApplication("test", context);
equal(result, null, `lookupApplication ignores missing ${prop}`);
}
});
add_task(function* test_invalid_type() {
let manifest = Object.assign({}, templateManifest);
manifest.type = "bogus";
yield writeManifest(USER_TEST_JSON, manifest);
let result = yield HostManifestManager.lookupApplication("test", context);
equal(result, null, "lookupApplication ignores invalid type");
});
add_task(function* test_no_allowed_extensions() {
let manifest = Object.assign({}, templateManifest);
manifest.allowed_extensions = [];
yield writeManifest(USER_TEST_JSON, manifest);
let result = yield HostManifestManager.lookupApplication("test", context);
equal(result, null, "lookupApplication ignores manifest with no allowed_extensions");
});
const GLOBAL_TEST_JSON = OS.Path.join(globalDir.path, "test.json");
let globalManifest = Object.assign({}, templateManifest);
globalManifest.description = "This manifest is from the systemwide directory";
add_task(function* good_manifest_system_dir() {
yield OS.File.remove(USER_TEST_JSON);
yield writeManifest(GLOBAL_TEST_JSON, globalManifest);
if (registry) {
registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
`${REGPATH}\\test`, "", null);
registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
`${REGPATH}\\test`, "", GLOBAL_TEST_JSON);
}
let where = (AppConstants.platform == "win") ? "registry location" : "directory";
let result = yield HostManifestManager.lookupApplication("test", context);
notEqual(result, null, `lookupApplication finds a manifest in the system-wide ${where}`);
equal(result.path, GLOBAL_TEST_JSON, `lookupApplication returns path in the system-wide ${where}`);
deepEqual(result.manifest, globalManifest, `lookupApplication returns manifest contents from the system-wide ${where}`);
});
add_task(function* test_user_dir_precedence() {
yield writeManifest(USER_TEST_JSON, templateManifest);
if (registry) {
registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
`${REGPATH}\\test`, "", USER_TEST_JSON);
}
// global test.json and LOCAL_MACHINE registry key on windows are
// still present from the previous test
let result = yield HostManifestManager.lookupApplication("test", context);
notEqual(result, null, "lookupApplication finds a manifest when entries exist in both user-specific and system-wide locations");
equal(result.path, USER_TEST_JSON, "lookupApplication returns the user-specific path when user-specific and system-wide entries both exist");
deepEqual(result.manifest, templateManifest, "lookupApplication returns user-specific manifest contents with user-specific and system-wide entries both exist");
});
// Test shutdown handling in NativeApp
add_task(function* test_native_app_shutdown() {
const SCRIPT = String.raw`
import signal
import struct
import sys
signal.signal(signal.SIGTERM, signal.SIG_IGN)
while True:
rawlen = sys.stdin.read(4)
if len(rawlen) == 0:
signal.pause()
msglen = struct.unpack('@I', rawlen)[0]
msg = sys.stdin.read(msglen)
sys.stdout.write(struct.pack('@I', msglen))
sys.stdout.write(msg)
`;
let scriptPath = OS.Path.join(userDir.path, "wontdie.py");
let manifestPath = OS.Path.join(userDir.path, "wontdie.json");
const ID = "native@tests.mozilla.org";
let manifest = {
name: "wontdie",
description: "test async shutdown of native apps",
type: "stdio",
allowed_extensions: [ID],
};
if (AppConstants.platform == "win") {
yield OS.File.writeAtomic(scriptPath, SCRIPT);
let batPath = OS.Path.join(userDir.path, "wontdie.bat");
let batBody = `@ECHO OFF\n${PYTHON} -u "${scriptPath}" %*\n`;
yield OS.File.writeAtomic(batPath, batBody);
yield OS.File.setPermissions(batPath, {unixMode: 0o755});
manifest.path = batPath;
yield writeManifest(manifestPath, manifest);
registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
`${REGPATH}\\wontdie`, "", manifestPath);
} else {
yield OS.File.writeAtomic(scriptPath, `#!${PYTHON} -u\n${SCRIPT}`);
yield OS.File.setPermissions(scriptPath, {unixMode: 0o755});
manifest.path = scriptPath;
yield writeManifest(manifestPath, manifest);
}
let mockContext = new MockContext(ID);
let app = new NativeApp(mockContext, "wontdie");
// send a message and wait for the reply to make sure the app is running
let MSG = "test";
let recvPromise = new Promise(resolve => {
let listener = (what, msg) => {
equal(msg, MSG, "Received test message");
app.off("message", listener);
resolve();
};
app.on("message", listener);
});
let buffer = NativeApp.encodeMessage(mockContext, MSG);
app.send(buffer);
yield recvPromise;
app._cleanup();
do_print("waiting for async shutdown");
Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
AsyncShutdown.profileBeforeChange._trigger();
Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
let procs = yield SubprocessImpl.Process.getWorker().call("getProcesses", []);
equal(procs.size, 0, "native process exited");
});

View file

@ -0,0 +1,69 @@
[DEFAULT]
head = head.js
tail =
firefox-appdir = browser
skip-if = appname == "thunderbird"
support-files =
data/** head_sync.js
tags = webextensions
[test_csp_custom_policies.js]
[test_csp_validator.js]
[test_ext_alarms.js]
[test_ext_alarms_does_not_fire.js]
[test_ext_alarms_periodic.js]
[test_ext_alarms_replaces.js]
[test_ext_apimanager.js]
[test_ext_api_permissions.js]
[test_ext_background_generated_load_events.js]
[test_ext_background_generated_reload.js]
[test_ext_background_global_history.js]
skip-if = os == "android" # Android does not use Places for history.
[test_ext_background_private_browsing.js]
[test_ext_background_runtime_connect_params.js]
[test_ext_background_sub_windows.js]
[test_ext_background_window_properties.js]
skip-if = os == "android"
[test_ext_contexts.js]
[test_ext_downloads.js]
[test_ext_downloads_download.js]
skip-if = os == "android"
[test_ext_downloads_misc.js]
skip-if = os == "android"
[test_ext_downloads_search.js]
skip-if = os == "android"
[test_ext_experiments.js]
skip-if = release_or_beta
[test_ext_extension.js]
[test_ext_idle.js]
[test_ext_json_parser.js]
[test_ext_localStorage.js]
[test_ext_management.js]
[test_ext_management_uninstall_self.js]
[test_ext_manifest_content_security_policy.js]
[test_ext_manifest_incognito.js]
[test_ext_manifest_minimum_chrome_version.js]
[test_ext_onmessage_removelistener.js]
[test_ext_runtime_connect_no_receiver.js]
[test_ext_runtime_getBrowserInfo.js]
[test_ext_runtime_getPlatformInfo.js]
[test_ext_runtime_onInstalled_and_onStartup.js]
[test_ext_runtime_sendMessage.js]
[test_ext_runtime_sendMessage_errors.js]
[test_ext_runtime_sendMessage_no_receiver.js]
[test_ext_runtime_sendMessage_self.js]
[test_ext_schemas.js]
[test_ext_schemas_api_injection.js]
[test_ext_schemas_async.js]
[test_ext_schemas_allowed_contexts.js]
[test_ext_simple.js]
[test_ext_storage.js]
[test_ext_topSites.js]
skip-if = os == "android"
[test_getAPILevelForWindow.js]
[test_ext_legacy_extension_context.js]
[test_ext_legacy_extension_embedding.js]
[test_locale_converter.js]
[test_locale_data.js]
[test_native_messaging.js]
skip-if = os == "android"