--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -624,25 +624,25 @@ GlobalManager = {
get cloneScope() {
return context.cloneScope;
},
hasPermission(permission) {
return context.extension.hasPermission(permission);
},
- callFunction(path, name, args) {
+ callFunction(pathObj, path, name, args) {
return findPathInObject(apis, path)[name](...args);
},
- callFunctionNoReturn(path, name, args) {
+ callFunctionNoReturn(pathObj, path, name, args) {
findPathInObject(apis, path)[name](...args);
},
- callAsyncFunction(path, name, args, callback) {
+ callAsyncFunction(pathObj, path, name, args, callback) {
// We pass an empty stub function as a default callback for
// the `chrome` API, so promise objects are not returned,
// and lastError values are reported immediately.
if (callback === null) {
callback = defaultCallback;
}
let promise;
@@ -657,31 +657,31 @@ GlobalManager = {
shouldInject(namespace, name) {
if (namespaces && !namespaces.includes(namespace)) {
return false;
}
return findPathInObject(apis, [namespace]) != null;
},
- getProperty(path, name) {
+ getProperty(pathObj, path, name) {
return findPathInObject(apis, path)[name];
},
- setProperty(path, name, value) {
+ setProperty(pathObj, path, name, value) {
findPathInObject(apis, path)[name] = value;
},
- addListener(path, name, listener, args) {
+ addListener(pathObj, path, name, listener, args) {
findPathInObject(apis, path)[name].addListener.call(null, listener, ...args);
},
- removeListener(path, name, listener) {
+ removeListener(pathObj, path, name, listener) {
findPathInObject(apis, path)[name].removeListener.call(null, listener);
},
- hasListener(path, name, listener) {
+ hasListener(pathObj, path, name, listener) {
return findPathInObject(apis, path)[name].hasListener.call(null, listener);
},
};
Schemas.inject(dest, schemaWrapper);
},
observe(document, topic, data) {
let contentWindow = document.defaultView;
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -1475,28 +1475,28 @@ class ChildAPIManager {
close() {
this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
}
get cloneScope() {
return this.context.cloneScope;
}
- callFunction(path, name, args) {
+ callFunction(pathObj, path, name, args) {
throw new Error("Not implemented");
}
- callFunctionNoReturn(path, name, args) {
+ callFunctionNoReturn(pathObj, path, name, args) {
this.messageManager.sendAsyncMessage("API:Call", {
childId: this.id,
path, name, args,
});
}
- callAsyncFunction(path, name, args, callback) {
+ callAsyncFunction(pathObj, path, name, args, callback) {
let callId = nextId++;
let deferred = PromiseUtils.defer();
this.callPromises.set(callId, deferred);
this.messageManager.sendAsyncMessage("API:Call", {
childId: this.id,
callId,
path, name, args,
@@ -1508,25 +1508,25 @@ class ChildAPIManager {
shouldInject(namespace, name) {
return this.namespaces.includes(namespace);
}
hasPermission(permission) {
return this.context.extension.permissions.has(permission);
}
- getProperty(path, name) {
+ getProperty(pathObj, path, name) {
throw new Error("Not implemented");
}
- setProperty(path, name, value) {
+ setProperty(pathObj, path, name, value) {
throw new Error("Not implemented");
}
- addListener(path, name, listener, args) {
+ addListener(pathObj, path, name, listener, args) {
let ref = path.concat(name).join(".");
let set;
if (this.listeners.has(ref)) {
set = this.listeners.get(ref);
} else {
set = new Set();
this.listeners.set(ref, set);
}
@@ -1538,30 +1538,30 @@ class ChildAPIManager {
this.messageManager.sendAsyncMessage("API:AddListener", {
childId: this.id,
path, name, args,
});
}
}
- removeListener(path, name, listener) {
+ removeListener(pathObj, path, name, listener) {
let ref = path.concat(name).join(".");
let set = this.listeners.get(ref) || new Set();
set.remove(listener);
if (set.size == 0) {
this.messageManager.sendAsyncMessage("Extension:RemoveListener", {
childId: this.id,
path, name,
});
}
}
- hasListener(path, name, listener) {
+ hasListener(pathObj, path, name, listener) {
let ref = path.concat(name).join(".");
let set = this.listeners.get(ref) || new Set();
return set.has(listener);
}
}
/**
* Convert any of several different representations of a date/time to a Date object.
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -89,16 +89,17 @@ const CONTEXT_FOR_VALIDATION = [
"hasPermission",
"logError",
];
// Methods of Context that are used by Schemas.inject.
// Callers of Schemas.inject should implement all of these methods.
const CONTEXT_FOR_INJECTION = [
...CONTEXT_FOR_VALIDATION,
+ "shouldInject",
"callFunction",
"callFunctionNoReturn",
"callAsyncFunction",
"getProperty",
"setProperty",
"addListener",
"hasListener",
@@ -335,118 +336,154 @@ class Context {
* as defined in the schema. Otherwise an error is reported to the context.
*/
class InjectionContext extends Context {
constructor(params) {
super(params, CONTEXT_FOR_INJECTION);
}
/**
+ * Called before injecting an API. The return value is used to determine
+ * whether to inject the API, and if so what the value of the `pathObj`
+ * parameter should be when the methods of this interface are called.
+ * - If falsey, the API is not injected.
+ * - If `true`, the API is injected and the `pathObj` parameter is `null`.
+ * - If an object, the `pathObj` parameter is this object. The object SHOULD
+ * have a property `name`.
+ *
+ * With the above contract, a local API implementation can simply be
+ * implemented as follows:
+ *
+ * callFunction(pathObj, path, name, args) {
+ * if (pathObj)
+ * return pathObj[name];
+ * // else the local API does not exist, so fall back or throw an error.
+ * }
+ *
+ * @abstract
+ * @param {string} namespace The namespace of the API. This may contain dots,
+ * e.g. in the case of "devtools.inspectedWindow".
+ * @param {string} [name] The name of the property in the namespace.
+ * @returns {*} An object with the property `name`, `true` or a falsey value.
+ */
+ shouldInject(namespace, name) {
+ throw new Error("Not implemented");
+ }
+
+ /**
* Calls function `path`.`name` and returns its return value.
*
* @abstract
+ * @param {*} pathObj See `shouldInject`.
* @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
* @param {string} name The method name, e.g. "get".
* @param {Array} args The parameters for the function.
* @returns {*} The return value of the invoked function.
*/
- callFunction(path, name, args) {
+ callFunction(pathObj, path, name, args) {
throw new Error("Not implemented");
}
/**
* Calls function `path`.`name` and ignores its return value.
*
* @abstract
+ * @param {*} pathObj See `shouldInject`.
* @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
* @param {string} name The method name, e.g. "get".
* @param {Array} args The parameters for the function.
*/
- callFunctionNoReturn(path, name, args) {
+ callFunctionNoReturn(pathObj, path, name, args) {
throw new Error("Not implemented");
}
/**
* Call function `path`.`name` that completes asynchronously.
*
* @abstract
+ * @param {*} pathObj See `shouldInject`.
* @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
* @param {string} name The method name, e.g. "get".
* @param {Array} args The parameters for the function.
* @param {function(*)} [callback] The callback to be called when the function
* completes.
* @returns {Promise|undefined} Must be void if `callback` is set, and a
* promise otherwise. The promise is resolved when the function completes.
*/
- callAsyncFunction(path, name, args, callback) {
+ callAsyncFunction(pathObj, path, name, args, callback) {
throw new Error("Not implemented");
}
/**
* Retrieves the value of property `path`.`name`.
*
* @abstract
+ * @param {*} pathObj See `shouldInject`.
* @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
* @param {string} name The property name.
* @returns {*} The value of the property.
*/
- getProperty(path, name) {
+ getProperty(pathObj, path, name) {
throw new Error("Not implemented");
}
/**
* Assigns the value of property `path`.`name`.
*
* @abstract
+ * @param {*} pathObj See `shouldInject`.
* @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
* @param {string} name The property name.
* @param {string} value The new value of the property.
*/
- setProperty(path, name, value) {
+ setProperty(pathObj, path, name, value) {
throw new Error("Not implemented");
}
/**
* Registers `listener` for event `path`.`name`.
*
* @abstract
+ * @param {*} pathObj See `shouldInject`.
* @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
* @param {string} name The event name, e.g. "onChanged"
* @param {function} listener The callback to be called when the event fires.
* @param {Array} args Extra parameters for EventManager.addListener.
* @see EventManager.addListener
*/
- addListener(path, name, listener, args) {
+ addListener(pathObj, path, name, listener, args) {
throw new Error("Not implemented");
}
/**
* Checks whether `listener` is listening to event `path`.`name`.
*
* @abstract
+ * @param {*} pathObj See `shouldInject`.
* @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
* @param {string} name The event name, e.g. "onChanged"
* @param {function} listener The event listener.
* @returns {boolean} Whether `listener` was added to event `path`.`name`.
* @see EventManager.hasListener
*/
- hasListener(path, name, listener) {
+ hasListener(pathObj, path, name, listener) {
throw new Error("Not implemented");
}
/**
* Unregisters `listener` from event `path`.`name`.
*
* @abstract
+ * @param {*} pathObj See `shouldInject`.
* @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
* @param {string} name The event name, e.g. "onChanged"
* @param {function} listener The event listener.
* @see EventManager.removeListener
*/
- removeListener(path, name, listener) {
+ removeListener(pathObj, path, name, listener) {
throw new Error("Not implemented");
}
}
/**
* The methods in this singleton represent the "format" specifier for
* JSON Schema string types.
@@ -607,22 +644,23 @@ class Entry {
}
/**
* Injects JS values for the entry into the extension API
* namespace. The default implementation is to do nothing.
* `context` is used to call the actual implementation
* of a given function or event.
*
+ * @param {*} pathObj See `shouldInject`.
* @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
* @param {string} name The method name, e.g. "get".
* @param {object} dest The object where `path`.`name` should be stored.
* @param {InjectionContext} context
*/
- inject(path, name, dest, context) {
+ inject(pathObj, path, name, dest, context) {
}
}
// Corresponds either to a type declared in the "types" section of the
// schema or else to any type object used throughout the schema.
class Type extends Entry {
// Takes a value, checks that it has the correct type, and returns a
// "normalized" version of the value. The normalized version will
@@ -803,17 +841,17 @@ class StringType extends Type {
return r;
}
checkBaseType(baseType) {
return baseType == "string";
}
- inject(path, name, dest, context) {
+ inject(pathObj, path, name, dest, context) {
if (this.enumeration) {
let obj = Cu.createObjectIn(dest, {defineAs: name});
for (let e of this.enumeration) {
let key = e.toUpperCase();
obj[key] = e;
}
}
}
@@ -1120,17 +1158,17 @@ class FunctionType extends Type {
// particular value. Essentially this is a constant.
class ValueProperty extends Entry {
constructor(schema, name, value) {
super(schema);
this.name = name;
this.value = value;
}
- inject(path, name, dest, context) {
+ inject(pathObj, path, name, dest, context) {
dest[name] = this.value;
}
}
// Represents a "property" defined in a schema namespace that is not a
// constant.
class TypeProperty extends Entry {
constructor(schema, namespaceName, name, type, writable) {
@@ -1140,41 +1178,41 @@ class TypeProperty extends Entry {
this.type = type;
this.writable = writable;
}
throwError(context, msg) {
throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`);
}
- inject(path, name, dest, context) {
+ inject(pathObj, path, name, dest, context) {
if (this.unsupported) {
return;
}
let getStub = () => {
this.checkDeprecated(context);
- return context.getProperty(path, name);
+ return context.getProperty(pathObj, path, name);
};
let desc = {
configurable: false,
enumerable: true,
get: Cu.exportFunction(getStub, dest),
};
if (this.writable) {
let setStub = (value) => {
let normalized = this.type.normalize(value, context);
if (normalized.error) {
this.throwError(context, normalized.error);
}
- context.setProperty(path, name, normalized.value);
+ context.setProperty(pathObj, path, name, normalized.value);
};
desc.set = Cu.exportFunction(setStub, dest);
}
Object.defineProperty(dest, name, desc);
}
}
@@ -1193,33 +1231,38 @@ class SubModuleProperty extends Entry {
constructor(name, namespaceName, reference, properties) {
super();
this.name = name;
this.namespaceName = namespaceName;
this.reference = reference;
this.properties = properties;
}
- inject(path, name, dest, context) {
+ inject(pathObj, path, name, dest, context) {
let obj = Cu.createObjectIn(dest, {defineAs: name});
let ns = Schemas.namespaces.get(this.namespaceName);
let type = ns.get(this.reference);
if (!type && this.reference.includes(".")) {
let [namespaceName, ref] = this.reference.split(".");
ns = Schemas.namespaces.get(namespaceName);
type = ns.get(ref);
}
if (!type || !(type instanceof SubModuleType)) {
throw new Error(`Internal error: ${this.namespaceName}.${this.reference} is not a sub-module`);
}
let functions = type.functions;
for (let fun of functions) {
- fun.inject(path.concat(name), fun.name, obj, context);
+ let subpath = path.concat(name);
+ let pathObj = context.shouldInject(subpath.join("."), fun.name);
+ if (pathObj) {
+ pathObj = pathObj === true ? null : pathObj;
+ fun.inject(pathObj, subpath, fun.name, obj, context);
+ }
}
// TODO: Inject this.properties.
}
}
// This class is a base class for FunctionEntrys and Events. It takes
// care of validating parameter lists (i.e., handling of optional
@@ -1317,17 +1360,17 @@ class FunctionEntry extends CallEntry {
this.unsupported = unsupported;
this.returns = returns;
this.permissions = permissions;
this.isAsync = type.isAsync;
this.hasAsyncCallback = type.hasAsyncCallback;
}
- inject(path, name, dest, context) {
+ inject(pathObj, path, name, dest, context) {
if (this.unsupported) {
return;
}
if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
return;
}
@@ -1335,29 +1378,29 @@ class FunctionEntry extends CallEntry {
if (this.isAsync) {
stub = (...args) => {
this.checkDeprecated(context);
let actuals = this.checkParameters(args, context);
let callback = null;
if (this.hasAsyncCallback) {
callback = actuals.pop();
}
- return context.callAsyncFunction(path, name, actuals, callback);
+ return context.callAsyncFunction(pathObj, path, name, actuals, callback);
};
} else if (!this.returns) {
stub = (...args) => {
this.checkDeprecated(context);
let actuals = this.checkParameters(args, context);
- return context.callFunctionNoReturn(path, name, actuals);
+ return context.callFunctionNoReturn(pathObj, path, name, actuals);
};
} else {
stub = (...args) => {
this.checkDeprecated(context);
let actuals = this.checkParameters(args, context);
- return context.callFunction(path, name, actuals);
+ return context.callFunction(pathObj, path, name, actuals);
};
}
Cu.exportFunction(stub, dest, {defineAs: name});
}
}
// Represents an "event" defined in a schema namespace.
class Event extends CallEntry {
@@ -1371,39 +1414,39 @@ class Event extends CallEntry {
checkListener(listener, context) {
let r = this.type.normalize(listener, context);
if (r.error) {
this.throwError(context, "Invalid listener");
}
return r.value;
}
- inject(path, name, dest, context) {
+ inject(pathObj, path, name, dest, context) {
if (this.unsupported) {
return;
}
if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
return;
}
let addStub = (listener, ...args) => {
listener = this.checkListener(listener, context);
let actuals = this.checkParameters(args, context);
- context.addListener(this.path, name, listener, actuals);
+ context.addListener(pathObj, this.path, name, listener, actuals);
};
let removeStub = (listener) => {
listener = this.checkListener(listener, context);
- context.removeListener(this.path, name, listener);
+ context.removeListener(pathObj, this.path, name, listener);
};
let hasStub = (listener) => {
listener = this.checkListener(listener, context);
- return context.hasListener(this.path, name, listener);
+ return context.hasListener(pathObj, this.path, name, listener);
};
let obj = Cu.createObjectIn(dest, {defineAs: name});
Cu.exportFunction(addStub, obj, {defineAs: "addListener"});
Cu.exportFunction(removeStub, obj, {defineAs: "removeListener"});
Cu.exportFunction(hasStub, obj, {defineAs: "hasListener"});
}
}
@@ -1822,18 +1865,20 @@ this.Schemas = {
for (let [namespace, ns] of this.namespaces) {
if (ns.permissions && !ns.permissions.some(perm => context.hasPermission(perm))) {
continue;
}
let obj = Cu.createObjectIn(dest, {defineAs: namespace});
for (let [name, entry] of ns) {
- if (wrapperFuncs.shouldInject(namespace, name)) {
- entry.inject([namespace], name, obj, context);
+ let pathObj = context.shouldInject(namespace, name);
+ if (pathObj) {
+ pathObj = pathObj === true ? null : pathObj;
+ entry.inject(pathObj, [namespace], name, obj, context);
}
}
// Remove the namespace object if it is empty
if (!Object.keys(obj).length) {
delete dest[namespace];
// process the next namespace.
continue;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -402,49 +402,49 @@ let wrapper = {
logError(message) {
talliedErrors.push(message);
},
hasPermission(permission) {
return permissions.has(permission);
},
- callFunction(path, name, args) {
+ callFunction(pathObj, path, name, args) {
let ns = path.join(".");
tally("call", ns, name, args);
},
- callFunctionNoReturn(path, name, args) {
+ callFunctionNoReturn(pathObj, path, name, args) {
let ns = path.join(".");
tally("call", ns, name, args);
},
shouldInject(ns) {
return ns != "do-not-inject";
},
- getProperty(path, name) {
+ getProperty(pathObj, path, name) {
let ns = path.join(".");
tally("get", ns, name);
},
- setProperty(path, name, value) {
+ setProperty(pathObj, path, name, value) {
let ns = path.join(".");
tally("set", ns, name, value);
},
- addListener(path, name, listener, args) {
+ addListener(pathObj, path, name, listener, args) {
let ns = path.join(".");
tally("addListener", ns, name, [listener, args]);
},
- removeListener(path, name, listener) {
+ removeListener(pathObj, path, name, listener) {
let ns = path.join(".");
tally("removeListener", ns, name, [listener]);
},
- hasListener(path, name, listener) {
+ hasListener(pathObj, path, name, listener) {
let ns = path.join(".");
tally("hasListener", ns, name, [listener]);
},
};
add_task(function* () {
let url = "data:," + JSON.stringify(json);
yield Schemas.load(url);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_pathObj.js
@@ -0,0 +1,153 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+
+function generateNsWithPropValue(val) {
+ let count = 0;
+ return {
+ get count() { return count; },
+ get prop1() {
+ ++count;
+ return val;
+ },
+ };
+}
+
+let pathObjApiJson = [
+ {
+ namespace: "ret_false",
+ properties: {
+ // Nb. Somehow a string is a valid value for the "object" type.
+ prop1: {type: "object", "value": "value of ret-false"},
+ },
+ },
+ {
+ namespace: "ret_false_noval",
+ properties: {
+ prop1: {type: "object"},
+ },
+ },
+ {
+ namespace: "dot.obj",
+ properties: {
+ prop1: {type: "object"},
+ },
+ },
+ {
+ namespace: "ret_obj",
+ properties: {
+ prop1: {type: "object"},
+ },
+ },
+ {
+ namespace: "ret_true",
+ properties: {
+ prop1: {type: "object", value: "value of ret-true"},
+ },
+ },
+ {
+ namespace: "ret_true_noval",
+ properties: {
+ prop1: {type: "object"},
+ },
+ },
+ {
+ namespace: "with_submodule",
+ types: [{
+ id: "subtype",
+ type: "object",
+ // Properties in submodules are not supported (yet), so we use functions.
+ functions: [{
+ name: "retObj",
+ type: "function",
+ parameters: [],
+ returns: {type: "string"},
+ }, {
+ name: "retFalse",
+ type: "function",
+ parameters: [],
+ returns: {type: "string"},
+ }, {
+ name: "retTrue",
+ type: "function",
+ parameters: [],
+ returns: {type: "string"},
+ }],
+ }],
+ properties: {
+ mySub: {$ref: "subtype"},
+ },
+ },
+];
+add_task(function* testPathObj() {
+ let url = "data:," + JSON.stringify(pathObjApiJson);
+ yield Schemas.load(url);
+
+ let localApi = generateNsWithPropValue("localApi value");
+ let dotApi = generateNsWithPropValue("dotApi value");
+ let submoduleApi = {
+ _count: 0,
+ retObj() {
+ return "submoduleApi value " + (++submoduleApi._count);
+ },
+ };
+ let localWrapper = {
+ shouldInject(ns, name) {
+ if (ns == "ret_obj") {
+ return localApi;
+ } else if (ns == "dot.obj") {
+ return dotApi;
+ } else if (ns == "ret_true" || ns == "ret_true_noval") {
+ return true;
+ } else if (ns == "ret_false" || ns == "ret_false_noval") {
+ return false;
+ } else if (ns == "with_submodule") {
+ if (name == "subtype" || name == "mySub") {
+ return true;
+ }
+ } else if (ns == "with_submodule.mySub") {
+ if (name == "retTrue") {
+ return true;
+ } else if (name == "retFalse") {
+ return false;
+ } else if (name == "retObj") {
+ return submoduleApi;
+ }
+ }
+ throw new Error(`Unexpected shouldInject call: ${ns} ${name}`);
+ },
+ getProperty(pathObj, path, name) {
+ if (pathObj) {
+ return pathObj[name];
+ }
+ return "fallback,pathObj=" + pathObj;
+ },
+ callFunction(pathObj, path, name, args) {
+ if (pathObj) {
+ return pathObj[name](...args);
+ }
+ return "fallback call,pathObj=" + pathObj;
+ },
+ };
+
+ let root = {};
+ Schemas.inject(root, localWrapper);
+
+ do_check_eq(0, localApi.count);
+ do_check_eq(0, dotApi.count);
+ do_check_eq(Object.keys(root).sort().join(),
+ "dot,ret_obj,ret_true,ret_true_noval,with_submodule");
+
+ do_check_eq("fallback,pathObj=null", root.ret_true_noval.prop1);
+ do_check_eq("value of ret-true", root.ret_true.prop1);
+ do_check_eq("localApi value", root.ret_obj.prop1);
+ do_check_eq(1, localApi.count);
+ do_check_eq("dotApi value", root.dot.obj.prop1);
+ do_check_eq(1, dotApi.count);
+
+ do_check_eq(Object.keys(root.with_submodule).sort().join(), "mySub");
+ let mySub = root.with_submodule.mySub;
+ do_check_eq(Object.keys(mySub).sort().join(), "retObj,retTrue");
+ do_check_eq("fallback call,pathObj=null", mySub.retTrue());
+ do_check_eq("submoduleApi value 1", mySub.retObj());
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -39,15 +39,16 @@ skip-if = release_build
[test_ext_onmessage_removelistener.js]
[test_ext_runtime_connect_no_receiver.js]
[test_ext_runtime_getPlatformInfo.js]
[test_ext_runtime_sendMessage.js]
[test_ext_runtime_sendMessage_errors.js]
[test_ext_runtime_sendMessage_no_receiver.js]
[test_ext_schemas.js]
[test_ext_schemas_api_injection.js]
+[test_ext_schemas_pathObj.js]
[test_ext_simple.js]
[test_ext_storage.js]
[test_getAPILevelForWindow.js]
[test_locale_converter.js]
[test_locale_data.js]
[test_native_messaging.js]
skip-if = os == "android"