Bug 1682668 - Improve error messages when TabDelegate is not present. r=owlish

Differential Revision: https://phabricator.services.mozilla.com/D99851
This commit is contained in:
Agi Sferro 2021-01-06 20:17:51 +00:00
parent d07e5d1640
commit 83036590b9
8 changed files with 164 additions and 47 deletions

View File

@ -8,6 +8,12 @@ const APIS = {
AddHistogram({ id, value }) {
browser.test.addHistogram(id, value);
},
Eval({ code }) {
// eslint-disable-next-line no-eval
return eval(`(async () => {
${code}
})()`);
},
SetScalar({ id, value }) {
browser.test.setScalar(id, value);
},
@ -59,10 +65,14 @@ browser.runtime.onConnect.addListener(contentPort => {
function apiCall(message, impl) {
const { id, args } = message;
sendResponse(id, impl(args));
try {
sendResponse(id, impl(args));
} catch (error) {
sendResponse(id, Promise.reject(error));
}
}
function sendResponse(id, response, exception) {
function sendResponse(id, response) {
Promise.resolve(response).then(
value => sendSyncResponse(id, value),
reason => sendSyncResponse(id, null, reason)

View File

@ -15,6 +15,7 @@
"run_at": "document_start"
}
],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';",
"background": {
"scripts": ["background.js"]
},
@ -29,6 +30,9 @@
}
}
},
"options_ui": {
"page": "dummy.html"
},
"permissions": [
"geckoViewAddons",
"nativeMessaging",

View File

@ -1281,6 +1281,46 @@ class GeckoSessionTestRuleTest : BaseSessionTest(noErrorCollector = true) {
sessionRule.session.waitForPageStop()
}
@Test fun evaluateExtensionJS() {
assertThat("JS string result should be correct",
sessionRule.evaluateExtensionJS("return 'foo';") as String, equalTo("foo"))
assertThat("JS number result should be correct",
sessionRule.evaluateExtensionJS("return 1+1;") as Double, equalTo(2.0))
assertThat("JS boolean result should be correct",
sessionRule.evaluateExtensionJS("return !0;") as Boolean, equalTo(true))
val expected = JSONObject("{bar:42,baz:true,foo:'bar'}")
val actual = sessionRule.evaluateExtensionJS("return {foo:'bar',bar:42,baz:true};") as JSONObject
for (key in expected.keys()) {
assertThat("JS object result should be correct",
actual.get(key), equalTo(expected.get(key)))
}
assertThat("JS array result should be correct",
sessionRule.evaluateExtensionJS("return [1,2,3];") as JSONArray,
equalTo(JSONArray("[1,2,3]")))
assertThat("Can access extension APIS",
sessionRule.evaluateExtensionJS("return !!browser.runtime;") as Boolean,
equalTo(true))
assertThat("Can access extension APIS",
sessionRule.evaluateExtensionJS("""
return true;
// Comments at the end are allowed""".trimIndent()) as Boolean,
equalTo(true))
try {
sessionRule.evaluateExtensionJS("test({ what")
assertThat("Should fail", true, equalTo(false))
} catch (e: RejectedPromiseException) {
assertThat("Syntax errors are reported",
e.message, containsString("SyntaxError"))
}
}
@Test fun evaluateJS() {
sessionRule.session.loadTestPath(HELLO_HTML_PATH);
sessionRule.session.waitForPageStop();

View File

@ -21,6 +21,7 @@ import org.mozilla.geckoview.WebExtensionController.EnableSource
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.Setting
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.RejectedPromiseException
import org.mozilla.geckoview.test.util.Callbacks
import org.mozilla.geckoview.test.util.RuntimeCreator
import org.mozilla.geckoview.test.util.UiThreadUtils
@ -139,6 +140,42 @@ class WebExtensionTest : BaseSessionTest() {
equalTo(blocklistDisabled))
}
@Test
fun noDelegateErrorMessage() {
try {
sessionRule.evaluateExtensionJS("""
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
await browser.tabs.update(tab.id, { url: "www.google.com" });
""")
assertThat("tabs.update should not succeed", true, equalTo(false))
} catch (ex: RejectedPromiseException) {
assertThat("Error message matches", ex.message,
equalTo("Error: tabs.update is not supported"))
}
try {
sessionRule.evaluateExtensionJS("""
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
await browser.tabs.remove(tab.id);
""")
assertThat("tabs.remove should not succeed", true, equalTo(false))
} catch (ex: RejectedPromiseException) {
assertThat("Error message matches", ex.message,
equalTo("Error: tabs.remove is not supported"))
}
try {
sessionRule.evaluateExtensionJS("""
await browser.runtime.openOptionsPage();
""")
assertThat("runtime.openOptionsPage should not succeed",
true, equalTo(false))
} catch (ex: RejectedPromiseException) {
assertThat("Error message matches", ex.message,
equalTo("Error: runtime.openOptionsPage is not supported"))
}
}
@Test
fun enableDisable() {
mainSession.loadUri("example.com")

View File

@ -1888,6 +1888,12 @@ public class GeckoSessionTestRule implements TestRule {
return new ExtensionPromise(UUID.randomUUID(), session, js);
}
public Object evaluateExtensionJS(final @NonNull String js) {
return webExtensionApiCall("Eval", args -> {
args.put("code", js);
});
}
public Object evaluateJS(final @NonNull GeckoSession session, final @NonNull String js) {
// Let's make sure we have the port already
UiThreadUtils.waitForCondition(() -> mPorts.containsKey(session), mTimeoutMillis);

View File

@ -1075,7 +1075,7 @@ public class WebExtensionController {
if (delegate != null) {
delegate.onOpenOptionsPage(extension);
} else {
// TODO: Save as pending?
message.callback.sendError("runtime.openOptionsPage is not supported");
}
message.callback.sendSuccess(null);
@ -1102,10 +1102,9 @@ public class WebExtensionController {
return;
}
result.accept(session -> {
message.callback.resolveTo(result.map(session -> {
if (session == null) {
message.callback.sendSuccess(null);
return;
return null;
}
if (session.isOpen()) {
@ -1114,8 +1113,8 @@ public class WebExtensionController {
session.open(mListener.runtime);
message.callback.sendSuccess(session.getId());
});
return session.getId();
}));
}
/* package */ void updateTab(final Message message, final WebExtension extension) {
@ -1124,19 +1123,21 @@ public class WebExtensionController {
final EventCallback callback = message.callback;
if (delegate == null) {
callback.sendError(null);
callback.sendError("tabs.update is not supported");
return;
}
delegate.onUpdateTab(extension, message.session,
new WebExtension.UpdateTabDetails(message.bundle.getBundle("updateProperties")))
.accept(value -> {
final WebExtension.UpdateTabDetails details =
new WebExtension.UpdateTabDetails(message.bundle.getBundle("updateProperties"));
callback.resolveTo(delegate
.onUpdateTab(extension, message.session, details)
.map(value -> {
if (value == AllowOrDeny.ALLOW) {
callback.sendSuccess(null);
return null;
} else {
callback.sendError(null);
throw new Exception("tabs.update is not supported");
}
});
}));
}
/* package */ void closeTab(final Message message,
@ -1151,13 +1152,13 @@ public class WebExtensionController {
result = GeckoResult.fromValue(AllowOrDeny.DENY);
}
result.accept(value -> {
message.callback.resolveTo(result.map(value -> {
if (value == AllowOrDeny.ALLOW) {
message.callback.sendSuccess(null);
return null;
} else {
message.callback.sendError(null);
throw new Exception("tabs.remove is not supported");
}
});
}));
}
/**
@ -1325,7 +1326,7 @@ public class WebExtensionController {
try {
content = message.bundle.toJSONObject().get("data");
} catch (JSONException ex) {
callback.sendError(ex);
callback.sendError(ex.getMessage());
return;
}

View File

@ -12,6 +12,11 @@ const { GeckoViewModule } = ChromeUtils.import(
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { ExtensionUtils } = ChromeUtils.import(
"resource://gre/modules/ExtensionUtils.jsm"
);
const { ExtensionError } = ExtensionUtils;
XPCOMUtils.defineLazyModuleGetters(this, {
EventDispatcher: "resource://gre/modules/Messaging.jsm",
@ -75,10 +80,17 @@ const GeckoViewTabBridge = {
async openOptionsPage(extensionId) {
debug`openOptionsPage for extensionId ${extensionId}`;
return EventDispatcher.instance.sendRequestForResult({
type: "GeckoView:WebExtension:OpenOptionsPage",
extensionId,
});
try {
await EventDispatcher.instance.sendRequestForResult({
type: "GeckoView:WebExtension:OpenOptionsPage",
extensionId,
});
} catch (errorMessage) {
// The error message coming from GeckoView is about :OpenOptionsPage not
// being registered so we need to have one that's extension friendly
// here.
throw new ExtensionError("runtime.openOptionsPage is not supported");
}
},
/**
@ -98,14 +110,21 @@ const GeckoViewTabBridge = {
async createNewTab({ extensionId, createProperties } = {}) {
debug`createNewTab`;
const sessionId = await EventDispatcher.instance.sendRequestForResult({
type: "GeckoView:WebExtension:NewTab",
extensionId,
createProperties,
});
let sessionId;
try {
sessionId = await EventDispatcher.instance.sendRequestForResult({
type: "GeckoView:WebExtension:NewTab",
extensionId,
createProperties,
});
} catch (errorMessage) {
// The error message coming from GeckoView is about :NewTab not being
// registered so we need to have one that's extension friendly here.
throw new ExtensionError("tabs.create is not supported");
}
if (!sessionId) {
throw new Error("Cannot create new tab");
throw new ExtensionError("Cannot create new tab");
}
const window = await new Promise(resolve => {
@ -143,18 +162,26 @@ const GeckoViewTabBridge = {
* Throws an error if the GeckoView app doesn't allow extension to close tab.
*/
async closeTab({ window, extensionId } = {}) {
await window.WindowEventDispatcher.sendRequestForResult({
type: "GeckoView:WebExtension:CloseTab",
extensionId,
});
try {
await window.WindowEventDispatcher.sendRequestForResult({
type: "GeckoView:WebExtension:CloseTab",
extensionId,
});
} catch (errorMessage) {
throw new ExtensionError(errorMessage);
}
},
async updateTab({ window, extensionId, updateProperties } = {}) {
await window.WindowEventDispatcher.sendRequestForResult({
type: "GeckoView:WebExtension:UpdateTab",
extensionId,
updateProperties,
});
try {
await window.WindowEventDispatcher.sendRequestForResult({
type: "GeckoView:WebExtension:UpdateTab",
extensionId,
updateProperties,
});
} catch (errorMessage) {
throw new ExtensionError(errorMessage);
}
},
};

View File

@ -95,14 +95,6 @@ class ExtensionActionHelper {
return window.WindowEventDispatcher;
}
sendRequestForResult(aTabId, aData) {
return this.eventDispatcherFor(aTabId).sendRequestForResult({
...aData,
aTabId,
extensionId: this.extension.id,
});
}
sendRequest(aTabId, aData) {
return this.eventDispatcherFor(aTabId).sendRequest({
...aData,