"use strict"; const {deepEqual, equal, ok} = require("chai").assert; const sinon = require("sinon"); const {setupTestDOMWindow} = require("./setup"); describe("browser-polyfill", () => { describe("proxies non-configurable read-only properties", () => { it("creates a proxy that doesn't raise a Proxy violation exception", () => { const fakeChrome = {"devtools": {}}; // Override the property to make it non-configurable (needed to be sure that // the polyfill is correctly workarounding the Proxy TypeError). 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"); }); }); }); describe("proxies non-wrapped functions", () => { it("should proxy non-wrapped methods", () => { const fakeChrome = { runtime: { nonwrappedmethod: sinon.spy(), }, }; return setupTestDOMWindow(fakeChrome).then(window => { ok(window.browser.runtime.nonwrappedmethod); const fakeCallback = () => {}; window.browser.runtime.nonwrappedmethod(fakeCallback); const receivedCallback = fakeChrome.runtime.nonwrappedmethod.firstCall.args[0]; equal(fakeCallback, receivedCallback, "The callback has not been wrapped for the nonwrappedmethod"); }); }); it("should proxy getters and setters", () => { const fakeChrome = { runtime: {myprop: "previous-value"}, nowrapns: { nowrapkey: "previous-value", nowrapkey2: "previous-value", }, }; return setupTestDOMWindow(fakeChrome).then(window => { // Check that the property values on the generated wrapper. equal(window.browser.runtime.myprop, "previous-value", "Got the expected result from setting a wrapped property name"); equal(window.browser.nowrapns.nowrapkey, "previous-value", "Got the expected result from setting a wrapped property name"); // Update the properties on the generated wrapper. const setResult = window.browser.runtime.myprop = "new-value"; const setResult2 = window.browser.nowrapns.nowrapkey = "new-value"; // Check the results of setting the new value of the wrapped properties. equal(setResult, "new-value", "Got the expected result from setting a wrapped property name"); equal(setResult2, "new-value", "Got the expected result from setting a wrapped property name"); // Verify that the wrapped properties has been updated. equal(window.browser.runtime.myprop, "new-value", "Got the expected updated value from the browser property"); equal(window.browser.nowrapns.nowrapkey, "new-value", "Got the expected updated value from the browser property"); // Verify that the target properties has been updated. equal(window.chrome.runtime.myprop, "new-value", "Got the expected updated value on the related chrome property"); equal(window.chrome.nowrapns.nowrapkey, "new-value", "Got the expected updated value on the related chrome property"); // Set a property multiple times before read. window.browser.nowrapns.nowrapkey2 = "new-value2"; window.browser.nowrapns.nowrapkey2 = "new-value3"; equal(window.chrome.nowrapns.nowrapkey2, "new-value3", "Got the expected updated value on the related chrome property"); equal(window.browser.nowrapns.nowrapkey2, "new-value3", "Got the expected updated value on the wrapped property"); }); }); it("deletes proxy getter/setter that are not wrapped", () => { const fakeChrome = {runtime: {}}; return setupTestDOMWindow(fakeChrome).then(window => { // 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"}; 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"); equal(window.browser.runtime.newns.newkey, "test-value", "Got the expected result from setting a wrapped property name"); const setRes = window.browser.runtime.newns = {newkey2: "new-value"}; equal(window.browser.runtime.newns.newkey2, "new-value", "The new non-wrapped getter is cached"); deepEqual(setRes, {newkey2: "new-value"}, "Got the expected result from setting a new wrapped property name"); deepEqual(window.browser.runtime.newns, window.chrome.runtime.newns, "chrome.newns and browser.newns are the same"); delete window.browser.runtime.newns.newkey2; equal(window.browser.runtime.newns.newkey2, undefined, "Got the expected result from setting a wrapped property name"); ok(!("newkey2" in window.browser.runtime.newns), "The deleted property is not listed anymore"); }); }); }); 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; }, get tabs() { ok(false, "chrome.tabs should not lazily be initialized without explicit API call"); }, }; return setupTestDOMWindow(fakeChrome).then(window => { // This used to be equal(lazyInitCount, 0, ...), but was changed to // accomodate a change in the implementation of the polyfill. // To verify that APIs are not unnecessarily initialized, the fakeChrome // object has a "tabs" getter that fails the test upon access. equal(lazyInitCount, 1, "chrome.runtime should be initialized because chrome.runtime.id is accessed during polyfill initialization"); 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"); }); }); }); describe("Privacy API", () => { it("Should wrap chrome.privacy.* API", () => { let lazyInitCount = 0; const fakeChrome = { privacy: { get network() { ++lazyInitCount; const networkPredictionEnabled = { get: () => {}, set: () => {}, clear: () => {}, }; return {networkPredictionEnabled}; }, }, }; return setupTestDOMWindow(fakeChrome).then(window => { equal(lazyInitCount, 0, "chrome.privacy.network is not accessed first"); const {get, set, clear} = window.browser.privacy.network.networkPredictionEnabled; equal(get({}).then !== undefined, true, "Privacy API get method is a Promise"); equal(set({}).then !== undefined, true, "Privacy API set method is a Promise"); equal(clear({}).then !== undefined, true, "Privacy API clear method is a Promise"); equal(lazyInitCount, 1, "chrome.privacy.network should be accessed only once"); window.browser.privacy.network.networkPredictionEnabled.get({}); equal(lazyInitCount, 1, "chrome.privacy.network should be accessed only once"); }); }); }); });