Skip to content

Commit

Permalink
Add WebExtension API for testing
Browse files Browse the repository at this point in the history
  • Loading branch information
toasted-nutbread committed Feb 23, 2020
1 parent d6454bc commit 5110de4
Show file tree
Hide file tree
Showing 16 changed files with 1,278 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"test": "npm run test-lint && npm run test-code",
"test-lint": "eslint .",
"test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js"
"test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-web-extension.js"
},
"repository": {
"type": "git",
Expand Down
310 changes: 310 additions & 0 deletions test/test-web-extension.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const WebExtension = require('./web-extension/web-extension.js').WebExtension;


function getAllFiles(directory) {
const results = [];
const directories = [directory];
while (directories.length > 0) {
const dir = directories.shift();
for (const fileName of fs.readdirSync(dir)) {
const fullFileName = path.join(dir, fileName);
const stat = fs.statSync(fullFileName);
if (stat.isDirectory()) {
directories.push(fullFileName);
} else if (stat.isFile()) {
results.push(fullFileName);
}
}
}
return results;
}

function getUsedWebExtensionApis() {
const apis = new Set();
for (const f of getAllFiles(path.join(__dirname, '..', 'ext'))) {
if (!(/\.js$/).test(f)) { continue; }

const source = fs.readFileSync(f, {encoding: 'utf8'});
const chromeApiPattern = /\bchrome\.([\w.]+)[(,; ]/g;
let m;
while ((m = chromeApiPattern.exec(source)) !== null) {
apis.add(m[1]);
}
}
return apis;
}

function getPropertyDescriptor(object, propertyName) {
try {
let pre = null;
while (object !== null && pre !== object) {
const descriptor = Object.getOwnPropertyDescriptor(object, propertyName);
if (typeof descriptor !== 'undefined') {
return descriptor;
}

pre = object;
object = Object.getPrototypeOf(object);
}
} catch (e) {
// NOP
}
return null;
}

function objectHasMember(root, memberPath) {
let pivot = root;
const parts = memberPath.split('.');
for (let i = 0, ii = parts.length; i < ii; ++i) {
const part = parts[i];
if (getPropertyDescriptor(pivot, part) === null) {
return false;
}
if (i + 1 < ii) {
pivot = pivot[part];
}
}
return true;
}

function getUndefinedWebExtensionAPIs(webExtensionObject) {
const apis = getUsedWebExtensionApis();
const missing = [];
for (const api of apis) {
if (!objectHasMember(webExtensionObject, api)) {
missing.push(api);
}
}
return missing;
}


function expectCallback(expectationList, expectedInvokeCount, executor) {
const expectation = {invokeCount: 0, expectedInvokeCount, executor};
const callback = () => ++expectation.invokeCount;
expectationList.push(expectation);
executor(callback);
}

async function checkCallbackExpectations(webExtension, expectationList) {
await webExtension.waitForDeferredActions(1000);
for (const expectation of expectationList) {
assert.ok(expectation.invokeCount === expectation.expectedInvokeCount, `Expected ${expectation.expectedInvokeCount} callback invocation(s) but received ${expectation.invokeCount} for: ${expectation.executor.toString()}`);
}
}


function testUndefinedWebExtensionAPIs() {
const webExtension = new WebExtension({
manifest: {},
backgroundPage: null
});
const chrome = webExtension.createContext(true, false);
const undefinedApis = getUndefinedWebExtensionAPIs(chrome);
assert.deepStrictEqual(undefinedApis, []);
}


async function testWebExtension() {
const manifest = {};
const backgroundPage = {};
const webExtension = new WebExtension({manifest, backgroundPage});
const testData = {manifest, backgroundPage};

const {context1, context2} = await testWebExtensionAPIs(webExtension, testData);
testData.context1 = context1;
testData.context2 = context2;

await testBrowserActionAPIs(webExtension, testData);
await testCommandsAPIs(webExtension, testData);
await testExtensionAPIs(webExtension, testData);
await testPermissionsAPIs(webExtension, testData);
await testRuntimeAPIs(webExtension, testData);
await testStorageAPIs(webExtension, testData);
await testTabsAPIs(webExtension, testData);
await testWindowsAPIs(webExtension, testData);
}

async function testWebExtensionAPIs(webExtension, {manifest, backgroundPage}) {
const context1 = webExtension.createContext(true, false);
assert.notStrictEqual(context1, null);

assert.throws(() => webExtension.createContext(true, false));

const context2 = webExtension.createContext(false, true, 0, 0);
assert.notStrictEqual(context2, null);

const context3 = webExtension.createContext(false, true, 0, 0);
assert.notStrictEqual(context3, null);
webExtension.removeContext(context3);

assert.deepStrictEqual(webExtension.getMessageSender(context1), {tab: null});
assert.deepStrictEqual(webExtension.getMessageSender(context2), {tab: {id: 0}, frameId: 0});

assert.deepStrictEqual(webExtension.getOtherContexts(context1), [context2]);
assert.deepStrictEqual(webExtension.getOtherContexts(context2), [context1]);
assert.deepStrictEqual(webExtension.getOtherContexts(context1, () => false), []);
assert.deepStrictEqual(webExtension.getOtherContexts(context2, () => false), []);

assert.strictEqual(webExtension.manifest, manifest);
assert.strictEqual(webExtension.backgroundPage, backgroundPage);

return {context1, context2};
}

async function testBrowserActionAPIs(webExtension, {context1}) {
const expectationList = [];

expectCallback(expectationList, 1, (cb) => context1.browserAction.setBadgeBackgroundColor({}, cb));
expectCallback(expectationList, 1, (cb) => context1.browserAction.setBadgeText({}, cb));

await checkCallbackExpectations(webExtension, expectationList);
}

async function testCommandsAPIs(webExtension, {context1}) {
const expectationList = [];

expectCallback(expectationList, 1, (cb) => context1.commands.onCommand.addListener(cb));
context1.commands.onCommand.internal.invoke();

await checkCallbackExpectations(webExtension, expectationList);
context1.commands.onCommand.internal.removeAllCallbacks();
}

async function testExtensionAPIs(webExtension, {context1, backgroundPage}) {
assert.strictEqual(context1.extension.getBackgroundPage(), backgroundPage);
}

async function testPermissionsAPIs(webExtension, {context1}) {
const expectationList = [];

expectCallback(expectationList, 1, (cb) => context1.permissions.request({}, cb));

await checkCallbackExpectations(webExtension, expectationList);
}

async function testRuntimeAPIs(webExtension, {context1, context2, manifest}) {
const expectationList = [];

assert.strictEqual(context1.runtime.lastError, void 0);

context1.runtime.onConnect.addListener((otherPort) => {
expectCallback(expectationList, 1, (cb) => otherPort.onMessage.addListener(cb));
expectCallback(expectationList, 1, (cb) => otherPort.onDisconnect.addListener(cb));
});
expectCallback(expectationList, 1, (cb) => context1.runtime.onConnect.addListener(cb));

const port1 = context2.runtime.connect({name: 'test1'});
assert.notStrictEqual(port1, null);
assert.strictEqual(port1.name, 'test1');
port1.postMessage({});
port1.disconnect();

const port2 = context1.runtime.connectNative('test2');
assert.notStrictEqual(port2, null);
assert.strictEqual(port2.name, 'test2');
port2.postMessage({});
port2.disconnect();

assert.strictEqual(context1.runtime.getManifest(), manifest);
expectCallback(expectationList, 1, (cb) => context1.runtime.getPlatformInfo(cb));

assert.notStrictEqual(context1.runtime.getURL('test'), null);

expectCallback(expectationList, 1, (cb) => context1.runtime.openOptionsPage(cb));

expectCallback(expectationList, 1, (cb) => context1.runtime.onMessage.addListener(cb));
expectCallback(expectationList, 1, (cb) => context2.runtime.sendMessage({}, {}, cb));

await checkCallbackExpectations(webExtension, expectationList);
context1.runtime.onMessage.internal.removeAllCallbacks();
context1.runtime.onConnect.internal.removeAllCallbacks();
}

async function testStorageAPIs(webExtension, {context1}) {
const expectationList = [];

assert.notStrictEqual(context1.storage.local, null);
expectCallback(expectationList, 1, (cb) => context1.storage.local.set({a: 1, b: 2, c: 3}, cb));
expectCallback(expectationList, 1, (cb) => context1.storage.local.get(['a', 'b', 'c'], cb));

context1.storage.local.get(['a', 'b', 'c'], (value) => {
assert.deepStrictEqual(value, {a: 1, b: 2, c: 3});
});

await checkCallbackExpectations(webExtension, expectationList);
}

async function testTabsAPIs(webExtension, {context1, context2}) {
const expectationList = [];


expectCallback(expectationList, 1, (cb) => context1.tabs.onZoomChange.addListener(cb));
context1.tabs.onZoomChange.internal.invoke();

expectCallback(expectationList, 1, (cb) => context1.tabs.captureVisibleTab(0, {}, cb));
expectCallback(expectationList, 1, (cb) => context1.tabs.create({}, cb));
expectCallback(expectationList, 1, (cb) => context1.tabs.getCurrent(cb));
expectCallback(expectationList, 1, (cb) => context1.tabs.getZoom(0, cb));
expectCallback(expectationList, 1, (cb) => context1.tabs.insertCSS(0, {}, cb));
expectCallback(expectationList, 1, (cb) => context1.tabs.query({}, cb));
expectCallback(expectationList, 1, (cb) => context1.tabs.query({}, cb));
expectCallback(expectationList, 1, (cb) => context1.tabs.update({}, {}, cb));

expectCallback(expectationList, 1, (cb) => context2.runtime.onMessage.addListener(cb));
expectCallback(expectationList, 1, (cb) => context1.tabs.sendMessage(0, {}, {}, cb));
expectCallback(expectationList, 1, (cb) => context1.tabs.sendMessage(0, {}, {frameId: 1}, cb)); // No receiver
expectCallback(expectationList, 1, (cb) => context1.tabs.sendMessage(1, {}, {}, cb)); // No receiver

context2.runtime.onConnect.addListener((otherPort) => {
expectCallback(expectationList, 1, (cb) => otherPort.onMessage.addListener(cb));
expectCallback(expectationList, 1, (cb) => otherPort.onDisconnect.addListener(cb));
});
expectCallback(expectationList, 1, (cb) => context2.runtime.onConnect.addListener(cb));

const port1 = context1.tabs.connect(0, {name: 'test1'});
assert.notStrictEqual(port1, null);
assert.strictEqual(port1.name, 'test1');
port1.postMessage({});
port1.disconnect();

const port2 = context1.tabs.connect(1, {name: 'test1'}); // No receiver
assert.notStrictEqual(port2, null);
assert.strictEqual(port2.name, 'test1');
port2.postMessage({});
port2.disconnect();

const port3 = context1.tabs.connect(0, {name: 'test1', frameId: 1}); // No receiver
assert.notStrictEqual(port3, null);
assert.strictEqual(port3.name, 'test1');
port3.postMessage({});
port3.disconnect();

await checkCallbackExpectations(webExtension, expectationList);
context1.tabs.onZoomChange.internal.removeAllCallbacks();
context2.runtime.onConnect.internal.removeAllCallbacks();
context2.runtime.onMessage.internal.removeAllCallbacks();
}

async function testWindowsAPIs(webExtension, {context1}) {
const expectationList = [];

expectCallback(expectationList, 1, (cb) => context1.windows.create({}, cb));
expectCallback(expectationList, 1, (cb) => context1.windows.get({}, {}, cb));
expectCallback(expectationList, 1, (cb) => context1.windows.remove({}, cb));
expectCallback(expectationList, 1, (cb) => context1.windows.update({}, {}, cb));

await checkCallbackExpectations(webExtension, expectationList);
}


async function main() {
testUndefinedWebExtensionAPIs();
await testWebExtension();
}


if (require.main === module) { main(); }
38 changes: 38 additions & 0 deletions test/web-extension/browser-action-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2020 Alex Yatskov <[email protected]>
* Author: Alex Yatskov <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

class WebExtensionBrowserActionAPI {
constructor(owner) {
this._owner = owner;
}

setBadgeBackgroundColor(details, callback) {
// NOP
this._owner.deferInvoke(callback);
}

setBadgeText(details, callback) {
// NOP
this._owner.deferInvoke(callback);
}
}


module.exports = {
WebExtensionBrowserActionAPI
};
34 changes: 34 additions & 0 deletions test/web-extension/commands-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2020 Alex Yatskov <[email protected]>
* Author: Alex Yatskov <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

const {WebExtensionEvent} = require('./event.js');


class WebExtensionCommandsAPI {
constructor(owner) {
this._owner = owner;
this._onCommand = new WebExtensionEvent();
}

get onCommand() { return this._onCommand; }
}


module.exports = {
WebExtensionCommandsAPI
};
Loading

0 comments on commit 5110de4

Please sign in to comment.