Skip to content

Commit

Permalink
Merge branch 'master' into fix/ms-edge
Browse files Browse the repository at this point in the history
  • Loading branch information
ExE-Boss committed Jun 4, 2018
2 parents dc836be + 5d186ba commit 320371f
Show file tree
Hide file tree
Showing 23 changed files with 821 additions and 210 deletions.
12 changes: 10 additions & 2 deletions .travis.yml
Expand Up @@ -2,7 +2,8 @@ language: node_js
sudo: false
node_js:
## Some of the ES6 syntax used in the browser-polyfill sources is only supported on nodejs >= 6
- '6'
## and the selenium-webdriver dependency used by the integration tests requires nodejs >= 8.
- '8'

script:
- npm run build
Expand All @@ -14,7 +15,14 @@ script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- echo "RUN integration tests on chrome" &&
TRAVIS_CI=true ./test/run-chrome-smoketests.sh
TRAVIS_CI=true ./test/run-browsers-smoketests.sh

## See https://docs.travis-ci.com/user/chrome
sudo: required

addons:
firefox: 'latest'
chrome: 'stable'

after_script: npm run publish-coverage

Expand Down
2 changes: 2 additions & 0 deletions Gruntfile.js
Expand Up @@ -99,6 +99,8 @@ module.exports = function(grunt) {
},
});

grunt.util.linefeed = "\n";

grunt.loadNpmTasks("gruntify-eslint");
grunt.loadNpmTasks("grunt-replace");
grunt.loadNpmTasks("grunt-coveralls");
Expand Down
14 changes: 11 additions & 3 deletions package.json
Expand Up @@ -22,9 +22,13 @@
"babel-plugin-transform-es2015-modules-umd": "^6.24.1",
"babel-preset-babili": "^0.0.10",
"babel-preset-es2017": "^6.24.1",
"browserify": "^16.2.2",
"chai": "^3.5.0",
"chromedriver": "^2.38.3",
"eslint": "^3.9.1",
"finalhandler": "^1.1.0",
"geckodriver": "^1.11.0",
"global-replaceify": "^1.0.0",
"grunt": "^1.0.1",
"grunt-babel": "^6.0.0",
"grunt-contrib-concat": "^1.0.1",
Expand All @@ -35,9 +39,13 @@
"jsdom": "^9.6.0",
"mocha": "^3.1.0",
"nyc": "^8.3.1",
"puppeteer": "^0.10.2",
"selenium-webdriver": "^4.0.0-alpha.1",
"serve-static": "^1.13.1",
"sinon": "^1.17.6"
"shelljs": "^0.8.2",
"sinon": "^1.17.6",
"tap-nirvana": "^1.0.8",
"tape-async": "^2.3.0",
"tmp": "0.0.33"
},
"nyc": {
"reporter": [
Expand All @@ -54,6 +62,6 @@
"test": "mocha",
"test-coverage": "COVERAGE=y nyc mocha",
"test-minified": "TEST_MINIFIED_POLYFILL=1 mocha",
"test-integration": "mocha -r test/mocha-babel test/integration/test-*"
"test-integration": "tape test/integration/test-*"
}
}
121 changes: 111 additions & 10 deletions src/browser-polyfill.js
Expand Up @@ -55,6 +55,13 @@ if (!isDefined(window, "browser") || !(() => {
}
return supportsPromises;
})()) {
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
removed from the specs (See
https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/onMessage)
`.replace(/\s+/g, " ").trim();

// Wrapping the bulk of this polyfill in a one-time-use function is a minor
// optimization for Firefox. Since Spidermonkey does not fully parse the
// contents of a function until the first time it's called, and since it will
Expand Down Expand Up @@ -134,6 +141,8 @@ if (!isDefined(window, "browser") || !(() => {
};
};

const pluralizeArguments = (numArgs) => numArgs == 1 ? "argument" : "arguments";

/**
* Creates a wrapper function for a method with the given name and metadata.
*
Expand All @@ -157,8 +166,6 @@ if (!isDefined(window, "browser") || !(() => {
* The generated wrapper function.
*/
const wrapAsyncFunction = (name, metadata) => {
const pluralizeArguments = (numArgs) => numArgs == 1 ? "argument" : "arguments";

return function asyncFunctionWrapper(target, ...args) {
if (args.length < metadata.minArgs) {
throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`);
Expand Down Expand Up @@ -375,6 +382,9 @@ if (!isDefined(window, "browser") || !(() => {
},
});

// Keep track if the deprecation warning has been logged at least once.
let loggedSendResponseDeprecationWarning = false;

const onMessageWrappers = new DefaultWeakMap(listener => {
if (typeof listener !== "function") {
return listener;
Expand All @@ -398,24 +408,115 @@ if (!isDefined(window, "browser") || !(() => {
* yield a response. False otherwise.
*/
return function onMessage(message, sender, sendResponse) {
let result = listener(message, sender);
let didCallSendResponse = false;

let wrappedSendResponse;
let sendResponsePromise = new Promise(resolve => {
wrappedSendResponse = function(response) {
if (!loggedSendResponseDeprecationWarning) {
console.warn(SEND_RESPONSE_DEPRECATION_WARNING, new Error().stack);
loggedSendResponseDeprecationWarning = true;
}
didCallSendResponse = true;
resolve(response);
};
});

let result;
try {
result = listener(message, sender, wrappedSendResponse);
} catch (err) {
result = Promise.reject(err);
}

if (isThenable(result)) {
result.then(sendResponse, error => {
console.error(error);
sendResponse(error);
const isResultThenable = result !== true && isThenable(result);

// If the listener didn't returned true or a Promise, or called
// wrappedSendResponse synchronously, we can exit earlier
// because there will be no response sent from this listener.
if (result !== true && !isResultThenable && !didCallSendResponse) {
return false;
}

// A small helper to send the message if the promise resolves
// and an error if the promise rejects (a wrapped sendMessage has
// to translate the message into a resolved promise or a rejected
// promise).
const sendPromisedResult = (promise) => {
promise.then(msg => {
// send the message value.
sendResponse(msg);
}, error => {
// Send a JSON representation of the error if the rejected value
// is an instance of error, or the object itself otherwise.
let message;
if (error && (error instanceof Error ||
typeof error.message === "string")) {
message = error.message;
} else {
message = "An unexpected error occurred";
}

sendResponse({
__mozWebExtensionPolyfillReject__: true,
message,
});
}).catch(err => {
// Print an error on the console if unable to send the response.
console.error("Failed to send onMessage rejected reply", err);
});
};

return true;
} else if (result !== undefined) {
sendResponse(result);
// If the listener returned a Promise, send the resolved value as a
// result, otherwise wait the promise related to the wrappedSendResponse
// callback to resolve and send it as a response.
if (isResultThenable) {
sendPromisedResult(result);
} else {
sendPromisedResult(sendResponsePromise);
}

// Let Chrome know that the listener is replying.
return true;
};
});

const wrappedSendMessageCallback = ({reject, resolve}, reply) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else if (reply && reply.__mozWebExtensionPolyfillReject__) {
// Convert back the JSON representation of the error into
// an Error instance.
reject(new Error(reply.message));
} else {
resolve(reply);
}
};

const wrappedSendMessage = (name, metadata, apiNamespaceObj, ...args) => {
if (args.length < metadata.minArgs) {
throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`);
}

if (args.length > metadata.maxArgs) {
throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`);
}

return new Promise((resolve, reject) => {
const wrappedCb = wrappedSendMessageCallback.bind(null, {resolve, reject});
args.push(wrappedCb);
apiNamespaceObj.sendMessage(...args);
});
};

const staticWrappers = {
runtime: {
onMessage: wrapEvent(onMessageWrappers),
onMessageExternal: wrapEvent(onMessageWrappers),
sendMessage: wrappedSendMessage.bind(null, "sendMessage", {minArgs: 1, maxArgs: 3}),
},
tabs: {
sendMessage: wrappedSendMessage.bind(null, "sendMessage", {minArgs: 2, maxArgs: 3}),
},
};

Expand Down
12 changes: 12 additions & 0 deletions test/fixtures/browserify-tape.js
@@ -0,0 +1,12 @@
const browserify = require("browserify");

const b = browserify();

b.add("./test/fixtures/tape-standalone.js");
b.transform("global-replaceify", {
global: true,
replacements: {
setImmediate: "require('timers').setImmediate",
},
});
b.bundle().pipe(process.stdout);
@@ -0,0 +1,18 @@
test("browser api object in content script", (t) => {
t.ok(browser && browser.runtime, "a global browser API object should be defined");
t.ok(chrome && chrome.runtime, "a global chrome API object should be defined");

if (navigator.userAgent.includes("Firefox/")) {
// Check that the polyfill didn't create a polyfill wrapped browser API object on Firefox.
t.equal(browser.runtime, chrome.runtime, "browser.runtime and chrome.runtime should be equal on Firefox");
// On Firefox, window is not the global object for content scripts, and so we expect window.browser to not
// be defined.
t.equal(window.browser, undefined, "window.browser is expected to be undefined on Firefox");
} else {
// Check that the polyfill has created a wrapped API namespace as expected.
t.notEqual(browser.runtime, chrome.runtime, "browser.runtime and chrome.runtime should not be equal");
// On chrome, window is the global object and so the polyfilled browser API should
// be also equal to window.browser.
t.equal(browser, window.browser, "browser and window.browser should be the same object");
}
});
@@ -0,0 +1,19 @@
{
"manifest_version": 2,
"name": "test-detect-browser-api-object-in-content-script",
"version": "0.1",
"description": "test-detect-browser-api-object-in-content-script",
"content_scripts": [
{
"matches": [
"http://localhost/*"
],
"js": [
"browser-polyfill.js",
"tape.js",
"content.js"
]
}
],
"permissions": []
}
39 changes: 39 additions & 0 deletions test/fixtures/multiple-onmessage-listeners-extension/background.js
@@ -0,0 +1,39 @@
console.log(name, "background page loaded");

async function testMessageHandler(msg, sender) {
console.log(name, "background received msg", {msg, sender});

// We only expect messages coming from a content script in this test.
if (!sender.tab || !msg.startsWith("test-multiple-onmessage-listeners:")) {
return {
success: false,
failureReason: `An unexpected message has been received: ${JSON.stringify({msg, sender})}`,
};
}

if (msg.endsWith(":resolve-to-undefined")) {
return undefined;
}

if (msg.endsWith(":resolve-to-null")) {
return null;
}

return {
success: false,
failureReason: `An unexpected message has been received: ${JSON.stringify({msg, sender})}`,
};
}

// Register the same message handler twice.
browser.runtime.onMessage.addListener(testMessageHandler);
browser.runtime.onMessage.addListener(testMessageHandler);

// Register an additional message handler that always reply after
// a small latency time.
browser.runtime.onMessage.addListener(async (msg, sender) => {
await new Promise(resolve => setTimeout(resolve, 100));
return "resolved-to-string-with-latency";
});

console.log(name, "background page ready to receive a content script message...");
18 changes: 18 additions & 0 deletions test/fixtures/multiple-onmessage-listeners-extension/content.js
@@ -0,0 +1,18 @@
test("Multiple runtime.onmessage listeners which resolve to undefined", async (t) => {
const res = await browser.runtime.sendMessage("test-multiple-onmessage-listeners:resolve-to-undefined");

if (navigator.userAgent.includes("Firefox/")) {
t.deepEqual(res, undefined, "Got an undefined value as expected");
} else {
// NOTE: When an onMessage listener sends `undefined` in a response,
// Chrome internally converts it to null and the receiver receives it
// as a null object.
t.deepEqual(res, null, "Got a null value as expected on Chrome");
}
});

test("Multiple runtime.onmessage listeners which resolve to null", async (t) => {
const res = await browser.runtime.sendMessage("test-multiple-onmessage-listeners:resolve-to-null");

t.deepEqual(res, null, "Got a null value as expected");
});
25 changes: 25 additions & 0 deletions test/fixtures/multiple-onmessage-listeners-extension/manifest.json
@@ -0,0 +1,25 @@
{
"manifest_version": 2,
"name": "test-multiple-onmessage-listeners",
"version": "0.1",
"description": "test-multiple-onmessage-listeners",
"content_scripts": [
{
"matches": [
"http://localhost/*"
],
"js": [
"browser-polyfill.js",
"tape.js",
"content.js"
]
}
],
"permissions": [],
"background": {
"scripts": [
"browser-polyfill.js",
"background.js"
]
}
}

0 comments on commit 320371f

Please sign in to comment.