Skip to content

Commit 5110de4

Browse files
Add WebExtension API for testing
1 parent d6454bc commit 5110de4

16 files changed

+1278
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"scripts": {
99
"test": "npm run test-lint && npm run test-code",
1010
"test-lint": "eslint .",
11-
"test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js"
11+
"test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-web-extension.js"
1212
},
1313
"repository": {
1414
"type": "git",

test/test-web-extension.js

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const assert = require('assert');
4+
const WebExtension = require('./web-extension/web-extension.js').WebExtension;
5+
6+
7+
function getAllFiles(directory) {
8+
const results = [];
9+
const directories = [directory];
10+
while (directories.length > 0) {
11+
const dir = directories.shift();
12+
for (const fileName of fs.readdirSync(dir)) {
13+
const fullFileName = path.join(dir, fileName);
14+
const stat = fs.statSync(fullFileName);
15+
if (stat.isDirectory()) {
16+
directories.push(fullFileName);
17+
} else if (stat.isFile()) {
18+
results.push(fullFileName);
19+
}
20+
}
21+
}
22+
return results;
23+
}
24+
25+
function getUsedWebExtensionApis() {
26+
const apis = new Set();
27+
for (const f of getAllFiles(path.join(__dirname, '..', 'ext'))) {
28+
if (!(/\.js$/).test(f)) { continue; }
29+
30+
const source = fs.readFileSync(f, {encoding: 'utf8'});
31+
const chromeApiPattern = /\bchrome\.([\w.]+)[(,; ]/g;
32+
let m;
33+
while ((m = chromeApiPattern.exec(source)) !== null) {
34+
apis.add(m[1]);
35+
}
36+
}
37+
return apis;
38+
}
39+
40+
function getPropertyDescriptor(object, propertyName) {
41+
try {
42+
let pre = null;
43+
while (object !== null && pre !== object) {
44+
const descriptor = Object.getOwnPropertyDescriptor(object, propertyName);
45+
if (typeof descriptor !== 'undefined') {
46+
return descriptor;
47+
}
48+
49+
pre = object;
50+
object = Object.getPrototypeOf(object);
51+
}
52+
} catch (e) {
53+
// NOP
54+
}
55+
return null;
56+
}
57+
58+
function objectHasMember(root, memberPath) {
59+
let pivot = root;
60+
const parts = memberPath.split('.');
61+
for (let i = 0, ii = parts.length; i < ii; ++i) {
62+
const part = parts[i];
63+
if (getPropertyDescriptor(pivot, part) === null) {
64+
return false;
65+
}
66+
if (i + 1 < ii) {
67+
pivot = pivot[part];
68+
}
69+
}
70+
return true;
71+
}
72+
73+
function getUndefinedWebExtensionAPIs(webExtensionObject) {
74+
const apis = getUsedWebExtensionApis();
75+
const missing = [];
76+
for (const api of apis) {
77+
if (!objectHasMember(webExtensionObject, api)) {
78+
missing.push(api);
79+
}
80+
}
81+
return missing;
82+
}
83+
84+
85+
function expectCallback(expectationList, expectedInvokeCount, executor) {
86+
const expectation = {invokeCount: 0, expectedInvokeCount, executor};
87+
const callback = () => ++expectation.invokeCount;
88+
expectationList.push(expectation);
89+
executor(callback);
90+
}
91+
92+
async function checkCallbackExpectations(webExtension, expectationList) {
93+
await webExtension.waitForDeferredActions(1000);
94+
for (const expectation of expectationList) {
95+
assert.ok(expectation.invokeCount === expectation.expectedInvokeCount, `Expected ${expectation.expectedInvokeCount} callback invocation(s) but received ${expectation.invokeCount} for: ${expectation.executor.toString()}`);
96+
}
97+
}
98+
99+
100+
function testUndefinedWebExtensionAPIs() {
101+
const webExtension = new WebExtension({
102+
manifest: {},
103+
backgroundPage: null
104+
});
105+
const chrome = webExtension.createContext(true, false);
106+
const undefinedApis = getUndefinedWebExtensionAPIs(chrome);
107+
assert.deepStrictEqual(undefinedApis, []);
108+
}
109+
110+
111+
async function testWebExtension() {
112+
const manifest = {};
113+
const backgroundPage = {};
114+
const webExtension = new WebExtension({manifest, backgroundPage});
115+
const testData = {manifest, backgroundPage};
116+
117+
const {context1, context2} = await testWebExtensionAPIs(webExtension, testData);
118+
testData.context1 = context1;
119+
testData.context2 = context2;
120+
121+
await testBrowserActionAPIs(webExtension, testData);
122+
await testCommandsAPIs(webExtension, testData);
123+
await testExtensionAPIs(webExtension, testData);
124+
await testPermissionsAPIs(webExtension, testData);
125+
await testRuntimeAPIs(webExtension, testData);
126+
await testStorageAPIs(webExtension, testData);
127+
await testTabsAPIs(webExtension, testData);
128+
await testWindowsAPIs(webExtension, testData);
129+
}
130+
131+
async function testWebExtensionAPIs(webExtension, {manifest, backgroundPage}) {
132+
const context1 = webExtension.createContext(true, false);
133+
assert.notStrictEqual(context1, null);
134+
135+
assert.throws(() => webExtension.createContext(true, false));
136+
137+
const context2 = webExtension.createContext(false, true, 0, 0);
138+
assert.notStrictEqual(context2, null);
139+
140+
const context3 = webExtension.createContext(false, true, 0, 0);
141+
assert.notStrictEqual(context3, null);
142+
webExtension.removeContext(context3);
143+
144+
assert.deepStrictEqual(webExtension.getMessageSender(context1), {tab: null});
145+
assert.deepStrictEqual(webExtension.getMessageSender(context2), {tab: {id: 0}, frameId: 0});
146+
147+
assert.deepStrictEqual(webExtension.getOtherContexts(context1), [context2]);
148+
assert.deepStrictEqual(webExtension.getOtherContexts(context2), [context1]);
149+
assert.deepStrictEqual(webExtension.getOtherContexts(context1, () => false), []);
150+
assert.deepStrictEqual(webExtension.getOtherContexts(context2, () => false), []);
151+
152+
assert.strictEqual(webExtension.manifest, manifest);
153+
assert.strictEqual(webExtension.backgroundPage, backgroundPage);
154+
155+
return {context1, context2};
156+
}
157+
158+
async function testBrowserActionAPIs(webExtension, {context1}) {
159+
const expectationList = [];
160+
161+
expectCallback(expectationList, 1, (cb) => context1.browserAction.setBadgeBackgroundColor({}, cb));
162+
expectCallback(expectationList, 1, (cb) => context1.browserAction.setBadgeText({}, cb));
163+
164+
await checkCallbackExpectations(webExtension, expectationList);
165+
}
166+
167+
async function testCommandsAPIs(webExtension, {context1}) {
168+
const expectationList = [];
169+
170+
expectCallback(expectationList, 1, (cb) => context1.commands.onCommand.addListener(cb));
171+
context1.commands.onCommand.internal.invoke();
172+
173+
await checkCallbackExpectations(webExtension, expectationList);
174+
context1.commands.onCommand.internal.removeAllCallbacks();
175+
}
176+
177+
async function testExtensionAPIs(webExtension, {context1, backgroundPage}) {
178+
assert.strictEqual(context1.extension.getBackgroundPage(), backgroundPage);
179+
}
180+
181+
async function testPermissionsAPIs(webExtension, {context1}) {
182+
const expectationList = [];
183+
184+
expectCallback(expectationList, 1, (cb) => context1.permissions.request({}, cb));
185+
186+
await checkCallbackExpectations(webExtension, expectationList);
187+
}
188+
189+
async function testRuntimeAPIs(webExtension, {context1, context2, manifest}) {
190+
const expectationList = [];
191+
192+
assert.strictEqual(context1.runtime.lastError, void 0);
193+
194+
context1.runtime.onConnect.addListener((otherPort) => {
195+
expectCallback(expectationList, 1, (cb) => otherPort.onMessage.addListener(cb));
196+
expectCallback(expectationList, 1, (cb) => otherPort.onDisconnect.addListener(cb));
197+
});
198+
expectCallback(expectationList, 1, (cb) => context1.runtime.onConnect.addListener(cb));
199+
200+
const port1 = context2.runtime.connect({name: 'test1'});
201+
assert.notStrictEqual(port1, null);
202+
assert.strictEqual(port1.name, 'test1');
203+
port1.postMessage({});
204+
port1.disconnect();
205+
206+
const port2 = context1.runtime.connectNative('test2');
207+
assert.notStrictEqual(port2, null);
208+
assert.strictEqual(port2.name, 'test2');
209+
port2.postMessage({});
210+
port2.disconnect();
211+
212+
assert.strictEqual(context1.runtime.getManifest(), manifest);
213+
expectCallback(expectationList, 1, (cb) => context1.runtime.getPlatformInfo(cb));
214+
215+
assert.notStrictEqual(context1.runtime.getURL('test'), null);
216+
217+
expectCallback(expectationList, 1, (cb) => context1.runtime.openOptionsPage(cb));
218+
219+
expectCallback(expectationList, 1, (cb) => context1.runtime.onMessage.addListener(cb));
220+
expectCallback(expectationList, 1, (cb) => context2.runtime.sendMessage({}, {}, cb));
221+
222+
await checkCallbackExpectations(webExtension, expectationList);
223+
context1.runtime.onMessage.internal.removeAllCallbacks();
224+
context1.runtime.onConnect.internal.removeAllCallbacks();
225+
}
226+
227+
async function testStorageAPIs(webExtension, {context1}) {
228+
const expectationList = [];
229+
230+
assert.notStrictEqual(context1.storage.local, null);
231+
expectCallback(expectationList, 1, (cb) => context1.storage.local.set({a: 1, b: 2, c: 3}, cb));
232+
expectCallback(expectationList, 1, (cb) => context1.storage.local.get(['a', 'b', 'c'], cb));
233+
234+
context1.storage.local.get(['a', 'b', 'c'], (value) => {
235+
assert.deepStrictEqual(value, {a: 1, b: 2, c: 3});
236+
});
237+
238+
await checkCallbackExpectations(webExtension, expectationList);
239+
}
240+
241+
async function testTabsAPIs(webExtension, {context1, context2}) {
242+
const expectationList = [];
243+
244+
245+
expectCallback(expectationList, 1, (cb) => context1.tabs.onZoomChange.addListener(cb));
246+
context1.tabs.onZoomChange.internal.invoke();
247+
248+
expectCallback(expectationList, 1, (cb) => context1.tabs.captureVisibleTab(0, {}, cb));
249+
expectCallback(expectationList, 1, (cb) => context1.tabs.create({}, cb));
250+
expectCallback(expectationList, 1, (cb) => context1.tabs.getCurrent(cb));
251+
expectCallback(expectationList, 1, (cb) => context1.tabs.getZoom(0, cb));
252+
expectCallback(expectationList, 1, (cb) => context1.tabs.insertCSS(0, {}, cb));
253+
expectCallback(expectationList, 1, (cb) => context1.tabs.query({}, cb));
254+
expectCallback(expectationList, 1, (cb) => context1.tabs.query({}, cb));
255+
expectCallback(expectationList, 1, (cb) => context1.tabs.update({}, {}, cb));
256+
257+
expectCallback(expectationList, 1, (cb) => context2.runtime.onMessage.addListener(cb));
258+
expectCallback(expectationList, 1, (cb) => context1.tabs.sendMessage(0, {}, {}, cb));
259+
expectCallback(expectationList, 1, (cb) => context1.tabs.sendMessage(0, {}, {frameId: 1}, cb)); // No receiver
260+
expectCallback(expectationList, 1, (cb) => context1.tabs.sendMessage(1, {}, {}, cb)); // No receiver
261+
262+
context2.runtime.onConnect.addListener((otherPort) => {
263+
expectCallback(expectationList, 1, (cb) => otherPort.onMessage.addListener(cb));
264+
expectCallback(expectationList, 1, (cb) => otherPort.onDisconnect.addListener(cb));
265+
});
266+
expectCallback(expectationList, 1, (cb) => context2.runtime.onConnect.addListener(cb));
267+
268+
const port1 = context1.tabs.connect(0, {name: 'test1'});
269+
assert.notStrictEqual(port1, null);
270+
assert.strictEqual(port1.name, 'test1');
271+
port1.postMessage({});
272+
port1.disconnect();
273+
274+
const port2 = context1.tabs.connect(1, {name: 'test1'}); // No receiver
275+
assert.notStrictEqual(port2, null);
276+
assert.strictEqual(port2.name, 'test1');
277+
port2.postMessage({});
278+
port2.disconnect();
279+
280+
const port3 = context1.tabs.connect(0, {name: 'test1', frameId: 1}); // No receiver
281+
assert.notStrictEqual(port3, null);
282+
assert.strictEqual(port3.name, 'test1');
283+
port3.postMessage({});
284+
port3.disconnect();
285+
286+
await checkCallbackExpectations(webExtension, expectationList);
287+
context1.tabs.onZoomChange.internal.removeAllCallbacks();
288+
context2.runtime.onConnect.internal.removeAllCallbacks();
289+
context2.runtime.onMessage.internal.removeAllCallbacks();
290+
}
291+
292+
async function testWindowsAPIs(webExtension, {context1}) {
293+
const expectationList = [];
294+
295+
expectCallback(expectationList, 1, (cb) => context1.windows.create({}, cb));
296+
expectCallback(expectationList, 1, (cb) => context1.windows.get({}, {}, cb));
297+
expectCallback(expectationList, 1, (cb) => context1.windows.remove({}, cb));
298+
expectCallback(expectationList, 1, (cb) => context1.windows.update({}, {}, cb));
299+
300+
await checkCallbackExpectations(webExtension, expectationList);
301+
}
302+
303+
304+
async function main() {
305+
testUndefinedWebExtensionAPIs();
306+
await testWebExtension();
307+
}
308+
309+
310+
if (require.main === module) { main(); }
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (C) 2020 Alex Yatskov <[email protected]>
3+
* Author: Alex Yatskov <[email protected]>
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
class WebExtensionBrowserActionAPI {
20+
constructor(owner) {
21+
this._owner = owner;
22+
}
23+
24+
setBadgeBackgroundColor(details, callback) {
25+
// NOP
26+
this._owner.deferInvoke(callback);
27+
}
28+
29+
setBadgeText(details, callback) {
30+
// NOP
31+
this._owner.deferInvoke(callback);
32+
}
33+
}
34+
35+
36+
module.exports = {
37+
WebExtensionBrowserActionAPI
38+
};

test/web-extension/commands-api.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (C) 2020 Alex Yatskov <[email protected]>
3+
* Author: Alex Yatskov <[email protected]>
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
const {WebExtensionEvent} = require('./event.js');
20+
21+
22+
class WebExtensionCommandsAPI {
23+
constructor(owner) {
24+
this._owner = owner;
25+
this._onCommand = new WebExtensionEvent();
26+
}
27+
28+
get onCommand() { return this._onCommand; }
29+
}
30+
31+
32+
module.exports = {
33+
WebExtensionCommandsAPI
34+
};

0 commit comments

Comments
 (0)