2016-10-10 00:48:35 +00:00
|
|
|
"use strict";
|
|
|
|
|
2016-10-12 00:08:52 +00:00
|
|
|
const {deepEqual, equal, ok} = require("chai").assert;
|
2016-10-11 14:39:13 +00:00
|
|
|
const sinon = require("sinon");
|
2016-10-10 00:48:35 +00:00
|
|
|
|
|
|
|
const {setupTestDOMWindow} = require("./setup");
|
|
|
|
|
|
|
|
describe("browser-polyfill", () => {
|
2017-09-22 21:38:44 +00:00
|
|
|
describe("proxies non-configurable read-only properties", () => {
|
|
|
|
it("creates a proxy that doesn't raise a Proxy violation exception", () => {
|
2017-10-11 13:51:43 +00:00
|
|
|
const fakeChrome = {"devtools": {}};
|
2017-09-22 21:38:44 +00:00
|
|
|
|
2017-10-11 13:51:43 +00:00
|
|
|
// Override the property to make it non-configurable (needed to be sure that
|
|
|
|
// the polyfill is correctly workarounding the Proxy TypeError).
|
2017-09-22 21:38:44 +00:00
|
|
|
Object.defineProperty(fakeChrome, "devtools", {
|
|
|
|
enumarable: true,
|
|
|
|
configurable: false,
|
|
|
|
writable: false,
|
|
|
|
value: {
|
|
|
|
inspectedWindow: {
|
|
|
|
eval: sinon.spy(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
return setupTestDOMWindow(fakeChrome).then(window => {
|
|
|
|
ok(window.browser.devtools.inspectedWindow,
|
|
|
|
"The non-configurable read-only property can be accessed");
|
|
|
|
|
|
|
|
const res = window.browser.devtools.inspectedWindow.eval("test");
|
|
|
|
|
|
|
|
ok(fakeChrome.devtools.inspectedWindow.eval.calledOnce,
|
|
|
|
"The target API method has been called once");
|
|
|
|
ok(res instanceof window.Promise, "The API method has been wrapped");
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2016-10-10 00:48:35 +00:00
|
|
|
describe("proxies non-wrapped functions", () => {
|
2016-10-11 14:39:13 +00:00
|
|
|
it("should proxy non-wrapped methods", () => {
|
|
|
|
const fakeChrome = {
|
|
|
|
runtime: {
|
|
|
|
nonwrappedmethod: sinon.spy(),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
return setupTestDOMWindow(fakeChrome).then(window => {
|
2016-10-12 00:08:52 +00:00
|
|
|
ok(window.browser.runtime.nonwrappedmethod);
|
2016-10-11 14:39:13 +00:00
|
|
|
|
|
|
|
const fakeCallback = () => {};
|
|
|
|
window.browser.runtime.nonwrappedmethod(fakeCallback);
|
|
|
|
|
|
|
|
const receivedCallback = fakeChrome.runtime.nonwrappedmethod.firstCall.args[0];
|
|
|
|
|
2016-10-12 00:08:52 +00:00
|
|
|
equal(fakeCallback, receivedCallback,
|
2016-11-02 19:39:07 +00:00
|
|
|
"The callback has not been wrapped for the nonwrappedmethod");
|
2016-10-11 14:39:13 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2016-10-10 00:48:35 +00:00
|
|
|
it("should proxy getters and setters", () => {
|
|
|
|
const fakeChrome = {
|
|
|
|
runtime: {myprop: "previous-value"},
|
2016-10-11 12:47:38 +00:00
|
|
|
nowrapns: {
|
|
|
|
nowrapkey: "previous-value",
|
|
|
|
nowrapkey2: "previous-value",
|
|
|
|
},
|
2016-10-10 00:48:35 +00:00
|
|
|
};
|
|
|
|
return setupTestDOMWindow(fakeChrome).then(window => {
|
2016-10-11 12:47:38 +00:00
|
|
|
// Check that the property values on the generated wrapper.
|
2016-10-12 00:08:52 +00:00
|
|
|
equal(window.browser.runtime.myprop, "previous-value",
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected result from setting a wrapped property name");
|
2016-10-12 00:08:52 +00:00
|
|
|
equal(window.browser.nowrapns.nowrapkey, "previous-value",
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected result from setting a wrapped property name");
|
2016-10-11 12:47:38 +00:00
|
|
|
|
|
|
|
// Update the properties on the generated wrapper.
|
2016-10-10 00:48:35 +00:00
|
|
|
const setResult = window.browser.runtime.myprop = "new-value";
|
|
|
|
const setResult2 = window.browser.nowrapns.nowrapkey = "new-value";
|
|
|
|
|
2016-10-11 12:47:38 +00:00
|
|
|
// Check the results of setting the new value of the wrapped properties.
|
2016-10-12 00:08:52 +00:00
|
|
|
equal(setResult, "new-value",
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected result from setting a wrapped property name");
|
2016-10-12 00:08:52 +00:00
|
|
|
equal(setResult2, "new-value",
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected result from setting a wrapped property name");
|
2016-10-11 12:47:38 +00:00
|
|
|
|
|
|
|
// Verify that the wrapped properties has been updated.
|
2016-10-12 00:08:52 +00:00
|
|
|
equal(window.browser.runtime.myprop, "new-value",
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected updated value from the browser property");
|
2016-10-12 00:08:52 +00:00
|
|
|
equal(window.browser.nowrapns.nowrapkey, "new-value",
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected updated value from the browser property");
|
2016-10-11 12:47:38 +00:00
|
|
|
|
|
|
|
// Verify that the target properties has been updated.
|
2016-10-12 00:08:52 +00:00
|
|
|
equal(window.chrome.runtime.myprop, "new-value",
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected updated value on the related chrome property");
|
2016-10-12 00:08:52 +00:00
|
|
|
equal(window.chrome.nowrapns.nowrapkey, "new-value",
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected updated value on the related chrome property");
|
2016-10-11 12:47:38 +00:00
|
|
|
|
|
|
|
// Set a property multiple times before read.
|
|
|
|
window.browser.nowrapns.nowrapkey2 = "new-value2";
|
|
|
|
window.browser.nowrapns.nowrapkey2 = "new-value3";
|
|
|
|
|
2016-10-12 00:08:52 +00:00
|
|
|
equal(window.chrome.nowrapns.nowrapkey2, "new-value3",
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected updated value on the related chrome property");
|
2016-10-12 00:08:52 +00:00
|
|
|
equal(window.browser.nowrapns.nowrapkey2, "new-value3",
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected updated value on the wrapped property");
|
2016-10-10 00:48:35 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2016-10-11 12:27:26 +00:00
|
|
|
it("deletes proxy getter/setter that are not wrapped", () => {
|
2017-09-22 21:38:44 +00:00
|
|
|
const fakeChrome = {runtime: {}};
|
2016-10-10 00:48:35 +00:00
|
|
|
return setupTestDOMWindow(fakeChrome).then(window => {
|
2017-09-22 21:38:44 +00:00
|
|
|
// Test getter/setter behavior for non wrapped properties on
|
|
|
|
// an API namespace (because the root target of the Proxy object
|
|
|
|
// is an empty object which has the chrome API object as its
|
|
|
|
// prototype and the empty object is not exposed outside of the
|
|
|
|
// polyfill sources).
|
|
|
|
window.browser.runtime.newns = {newkey: "test-value"};
|
2016-10-11 14:39:13 +00:00
|
|
|
|
2017-09-22 21:38:44 +00:00
|
|
|
ok("newns" in window.browser.runtime, "The custom namespace is in the wrapper");
|
|
|
|
ok("newns" in window.chrome.runtime, "The custom namespace is in the target");
|
2016-10-11 14:39:13 +00:00
|
|
|
|
2017-09-22 21:38:44 +00:00
|
|
|
equal(window.browser.runtime.newns.newkey, "test-value",
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected result from setting a wrapped property name");
|
2016-10-10 00:48:35 +00:00
|
|
|
|
2017-09-22 21:38:44 +00:00
|
|
|
const setRes = window.browser.runtime.newns = {newkey2: "new-value"};
|
|
|
|
equal(window.browser.runtime.newns.newkey2, "new-value",
|
2016-11-02 19:39:07 +00:00
|
|
|
"The new non-wrapped getter is cached");
|
2016-10-12 00:08:52 +00:00
|
|
|
deepEqual(setRes, {newkey2: "new-value"},
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected result from setting a new wrapped property name");
|
2017-09-22 21:38:44 +00:00
|
|
|
deepEqual(window.browser.runtime.newns, window.chrome.runtime.newns,
|
2016-11-02 19:39:07 +00:00
|
|
|
"chrome.newns and browser.newns are the same");
|
2016-10-10 00:48:35 +00:00
|
|
|
|
2017-09-22 21:38:44 +00:00
|
|
|
delete window.browser.runtime.newns.newkey2;
|
|
|
|
equal(window.browser.runtime.newns.newkey2, undefined,
|
2016-11-02 19:39:07 +00:00
|
|
|
"Got the expected result from setting a wrapped property name");
|
2017-09-22 21:38:44 +00:00
|
|
|
ok(!("newkey2" in window.browser.runtime.newns),
|
2016-11-02 19:39:07 +00:00
|
|
|
"The deleted property is not listed anymore");
|
2016-10-10 00:48:35 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
fix: Lazily initialize API via the original target (#71)
Originally, the polyfill created a Proxy with the original API object as
the target. This was changed to `Object.create(chrome)` because not
doing so would prevent the `browser.devtools` API from working because
the devtools API object is assigned as a read-only & non-configurable
property (#57).
However, that action itself caused a new bug: Whenever an API object
is dereferenced via the `browser` namespace, the original API is no
longer available in the `chrome` namespace, and trying to access the
API through `chrome` returns `undefined` plus the "Previous API
instantiation failed" warning (#58).
This is because Chrome lazily initializes fields in the `chrome`
API, but on the object from which the property is accessed, while
the polyfill accessed the property through an object with the
prototype set to `chrome` instead of directly via chrome.
To fix that, `Object.create(chrome)` was replaced with
`Object.assign({}, chrome)`. This fixes both of the previous issues
because
1) It is still a new object.
2) All lazily initialized fields are explicitly initialized.
This fix created a new issue: In Chrome some APIs cannot be used even
though they are visible in the API (e.g. `chrome.clipboard`), so
calling `Object.assign({}, chrome)` causes an error to be printed to
the console (#70).
To solve this, I use `Object.create(chrome)` again as a proxy target,
but dereference the API via the original target (`chrome`) to not
regress on #58.
Besides fixing the bug, this also reduces the performance impact
of the API because all API fields are lazily initialized again,
instead of upon start-up.
This fixes #70.
2018-03-12 18:23:28 +00:00
|
|
|
|
|
|
|
describe("without side effects", () => {
|
|
|
|
it("should proxy non-wrapped methods", () => {
|
|
|
|
let lazyInitCount = 0;
|
|
|
|
const fakeChrome = {
|
|
|
|
get runtime() {
|
|
|
|
// Chrome lazily initializes API objects by replacing the getter with
|
|
|
|
// the value. The initialization is only allowed to occur once,
|
|
|
|
// after that `undefined` is returned and a warning is printed.
|
|
|
|
// https://chromium.googlesource.com/chromium/src/+/4d6b3a067994ce6dcf0ed9a9efd566c083736952/extensions/renderer/module_system.cc#414
|
|
|
|
//
|
|
|
|
// The polyfill should invoke the getter only once (on the global chrome object).
|
|
|
|
++lazyInitCount;
|
|
|
|
|
|
|
|
const onMessage = {
|
|
|
|
addListener(listener) {
|
|
|
|
equal(this, onMessage, "onMessage.addListener should be called on the original chrome.onMessage object");
|
|
|
|
},
|
|
|
|
};
|
|
|
|
const value = {onMessage};
|
|
|
|
Object.defineProperty(this, "runtime", {value});
|
|
|
|
return value;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
return setupTestDOMWindow(fakeChrome).then(window => {
|
|
|
|
equal(lazyInitCount, 0, "chrome.runtime should not be initialized without explicit API call");
|
|
|
|
|
|
|
|
window.browser.runtime.onMessage.addListener(() => {});
|
|
|
|
equal(lazyInitCount, 1, "chrome.runtime should be initialized upon accessing browser.runtime");
|
|
|
|
|
|
|
|
window.browser.runtime.onMessage.addListener(() => {});
|
|
|
|
equal(lazyInitCount, 1, "chrome.runtime should be re-used upon accessing browser.runtime");
|
|
|
|
|
|
|
|
window.chrome.runtime.onMessage.addListener(() => {});
|
|
|
|
equal(lazyInitCount, 1, "chrome.runtime should be re-used upon accessing chrome.runtime");
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2016-10-10 00:48:35 +00:00
|
|
|
});
|