mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 15:58:39 +09:00
Rename Toolkit's webextensions component directory to better reflect what it is.
This commit is contained in:
parent
6955450f7a
commit
d7d16e7fbb
242 changed files with 4 additions and 2 deletions
|
|
@ -0,0 +1,9 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = { // eslint-disable-line no-undef
|
||||
"extends": "../../../../../testing/xpcshell/xpcshell.eslintrc.js",
|
||||
|
||||
"globals": {
|
||||
"browser": false,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE HTML>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div>Download HTML File</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1 @@
|
|||
This is a sample file used in download tests.
|
||||
111
toolkit/components/webextensions/test/xpcshell/head.js
Normal file
111
toolkit/components/webextensions/test/xpcshell/head.js
Normal 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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
67
toolkit/components/webextensions/test/xpcshell/head_sync.js
Normal file
67
toolkit/components/webextensions/test/xpcshell/head_sync.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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`);
|
||||
}
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
@ -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();
|
||||
});
|
||||
202
toolkit/components/webextensions/test/xpcshell/test_ext_idle.js
Normal file
202
toolkit/components/webextensions/test/xpcshell/test_ext_idle.js
Normal 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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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`);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
1427
toolkit/components/webextensions/test/xpcshell/test_ext_schemas.js
Normal file
1427
toolkit/components/webextensions/test/xpcshell/test_ext_schemas.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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.");
|
||||
});
|
||||
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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, "");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
69
toolkit/components/webextensions/test/xpcshell/xpcshell.ini
Normal file
69
toolkit/components/webextensions/test/xpcshell/xpcshell.ini
Normal 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue