Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for browsers which don’t support Promises #114

Closed
wants to merge 10 commits into from
68 changes: 54 additions & 14 deletions src/browser-polyfill.js
Expand Up @@ -6,7 +6,58 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

if (typeof browser === "undefined") {
/**
* Returns true if the given object is defined and has an entry with
* the name `key` and the value of object[key] isn’t undefined.
*
* @param {*} object The object to test.
* @param {*} key The entry to test for.
* @returns {boolean} True if the value for obj[key] is defined.
*/
const isDefined = (object, key) => {
if (object === window && window !== this) {
object = this;
}
if (typeof object !== "object" || object === null) {
return false;
}
return key in object && typeof object[key] !== "undefined";
};

/**
* Returns true if the given object is an object with a `then` method, and can
* therefore be assumed to behave as a Promise.
*
* @param {*} value The value to test.
* @returns {boolean} True if the value is thenable.
*/
const isThenable = value => {
return value && typeof value === "object" && typeof value.then === "function";
};

if (typeof browser === "undefined" || !(() => {
// If `browser.runtime.lastError` doesn’t exist, assume promies are supported.
let supportsPromises = !(isDefined(window.browser, "runtime")
? isDefined(window.browser.runtime, "lastError")
: isDefined(window.browser, "extension")
&& isDefined(window.browser.extension, "lastError"));
if (supportsPromises) {
return true;
}
if (!supportsPromises) {
try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s not need to nest this. If there are no errors, the function will return before the second try/catch

supportsPromises |= isThenable(window.browser.runtime.getPlatformInfo());
} catch (e) {
try {
// Microsoft Edge doesn’t support `browser.runtime.getPlatformInfo()`
supportsPromises |= isThenable(window.browser.windows.get(browser.windows.WINDOW_ID_CURRENT));
} catch (e2) {
// Do nothing.
}
}
}
return supportsPromises;
})()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a bit wordy, how about this function instead?

function supportsPromises() {
  if (typeof browser === "undefined") {
    return false;
  }
  // If `browser.runtime.lastError` doesn’t exist, assume promises are supported.
  if (browser.runtime && typeof browser.runtime.lastError !== "undefined") {
    return true;
  }
  if (browser.extension && typeof browser.extension.lastError !== "undefined") {
    return true;
  }
  try {
    return isThenable(browser.runtime.getPlatformInfo());
  } catch (error) {}
  try {
    return isThenable(browser.windows.get(browser.windows.WINDOW_ID_CURRENT));
  } catch (error) {}
  return false;
}

if (supportsPromises()) {

Copy link
Contributor Author

@ExE-Boss ExE-Boss Jun 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

browser.runtime && typeof browser.runtime.lastError !== "undefined" returns true if browser.runtime.lastError exists and false if it doesn’t.

Also, the check needs to check for the case when browser.runtime doesn’t exist, but browser.extension does, and consider the check failed if browser.runtime.lastError exists, but browser.extension.lastError doesn’t for whatever reason.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just change ! to =

Copy link
Contributor

@fregante fregante Jun 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider the check failed if browser.runtime.lastError exists, but browser.extension.lastError doesn’t for whatever reason.

That’s not what your code does. The ternary operator checks for one runtime and then discards extension

Copy link
Contributor Author

@ExE-Boss ExE-Boss Jun 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, yours discards the one in extension if none exists in runtime.

Also, yours fails the following test:

it("wraps the global browser namespace if it doesn’t support promises", () => {
const fakePlatformInfo = {
os: "test",
arch: "test",
};
const fakeBrowser = {
runtime: {
lastError: null,
getPlatformInfo: (cb) => {
if (typeof cb !== "function") {
throw new Error(cb + " is not a callback");
}
cb(fakePlatformInfo);
},
},
};
return setupTestDOMWindow(undefined, fakeBrowser).then(window => {
console.log(window.browser.runtime.getPlatformInfo());
notEqual(window.browser, fakeBrowser,
"The existing browser has been wrapped");
return Promise.all([
window,
window.browser.runtime.getPlatformInfo(),
]);
}).then(([window, platformInfo]) => {
deepEqual(platformInfo, fakePlatformInfo,
"The wrapped browser returns the expected value");
});
});

Edit: Turns out I forgot to change the !==s to ===.

Copy link
Contributor Author

@ExE-Boss ExE-Boss Jun 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #114 (comment):

Oh great, and now Firefox stopped working, again.

const SEND_RESPONSE_DEPRECATION_WARNING = `
Returning a Promise is the preferred way to send a reply from an
onMessage/onMessageExternal listener, as the sendResponse will be
Expand All @@ -19,7 +70,7 @@ if (typeof browser === "undefined") {
// contents of a function until the first time it's called, and since it will
// never actually need to be called, this allows the polyfill to be included
// in Firefox nearly for free.
const wrapAPIs = () => {
const wrapAPIs = chrome => {
// NOTE: apiMetadata is associated to the content of the api-metadata.json file
// at build time by replacing the following "include" with the content of the
// JSON file.
Expand Down Expand Up @@ -54,17 +105,6 @@ if (typeof browser === "undefined") {
}
}

/**
* Returns true if the given object is an object with a `then` method, and can
* therefore be assumed to behave as a Promise.
*
* @param {*} value The value to test.
* @returns {boolean} True if the value is thenable.
*/
const isThenable = value => {
return value && typeof value === "object" && typeof value.then === "function";
};

/**
* Creates and returns a function which, when called, will resolve or reject
* the given promise based on how it is called:
Expand Down Expand Up @@ -488,7 +528,7 @@ if (typeof browser === "undefined") {

// The build process adds a UMD wrapper around this file, which makes the
// `module` variable available.
module.exports = wrapAPIs(); // eslint-disable-line no-undef
module.exports = wrapAPIs(typeof browser !== "undefined" ? browser : chrome); // eslint-disable-line no-undef
} else {
module.exports = browser; // eslint-disable-line no-undef
}
36 changes: 34 additions & 2 deletions test/test-browser-global.js
@@ -1,6 +1,6 @@
"use strict";

const {deepEqual, equal, ok} = require("chai").assert;
const {deepEqual, equal, notEqual, ok} = require("chai").assert;

const {setupTestDOMWindow} = require("./setup");

Expand All @@ -12,7 +12,7 @@ describe("browser-polyfill", () => {
});
});

it("does not override the global browser namespace if it already exists", () => {
it("does not override the global browser namespace if it already exists and supports promises", () => {
const fakeChrome = {
runtime: {lastError: null},
};
Expand All @@ -26,6 +26,38 @@ describe("browser-polyfill", () => {
});
});

it("wraps the global browser namespace if it doesn’t support promises", () => {
const fakePlatformInfo = {
os: "test",
arch: "test",
};

const fakeBrowser = {
runtime: {
lastError: null,
getPlatformInfo: (cb) => {
if (typeof cb !== "function") {
throw new Error(cb + " is not a callback");
}
cb(fakePlatformInfo);
},
},
};

return setupTestDOMWindow(undefined, fakeBrowser).then(window => {
console.log(window.browser.runtime.getPlatformInfo());
notEqual(window.browser, fakeBrowser,
"The existing browser has been wrapped");
return Promise.all([
window,
window.browser.runtime.getPlatformInfo(),
]);
}).then(([window, platformInfo]) => {
deepEqual(platformInfo, fakePlatformInfo,
"The wrapped browser returns the expected value");
});
});

describe("browser wrapper", () => {
it("supports custom properties defined using Object.defineProperty", () => {
const fakeChrome = {};
Expand Down