├── src ├── css │ ├── README.md │ └── common.css ├── html │ ├── README.md │ ├── devtools.html │ ├── popup.html │ └── options.html ├── images │ ├── README.md │ └── icon.png ├── js │ ├── modules │ │ ├── README.md │ │ ├── circleci.node.fix.spec.js │ │ ├── chrome.devtools.mock.js │ │ ├── chrome.devtools.mock.spec.js │ │ ├── handlers.spec.js │ │ ├── handlers.js │ │ ├── runner.spec.js │ │ ├── chrome0.runtime │ │ ├── chrome.runtime.mock.js │ │ ├── runner.js │ │ ├── form.js │ │ ├── chrome.runtime.mock.spec.js │ │ ├── msg.js │ │ └── msg.spec.js │ ├── README.md │ ├── devTools.js │ ├── content.js │ ├── popup.js │ ├── options.js │ └── background.js ├── README.md └── manifest.json ├── .gitignore ├── circle.yml ├── .babelrc ├── webpack ├── webpack.config.test.js ├── config.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── .eslintrc ├── LICENSE ├── package.json └── README.md /src/css/README.md: -------------------------------------------------------------------------------- 1 | Put your CSS files here. 2 | -------------------------------------------------------------------------------- /src/html/README.md: -------------------------------------------------------------------------------- 1 | Put your HTML pages here. 2 | -------------------------------------------------------------------------------- /src/images/README.md: -------------------------------------------------------------------------------- 1 | Put all your graphic assets into this directory. 2 | -------------------------------------------------------------------------------- /src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsita/chrome-extension-skeleton/HEAD/src/images/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | mykey.pem 3 | node_modules 4 | npm-debug.log 5 | build/*.crx 6 | build/dev 7 | build/prod 8 | build/updates.xml 9 | .tmp 10 | *.swp 11 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | override: 3 | - npm install 4 | cache_directories: 5 | - node_modules 6 | 7 | test: 8 | override: 9 | - webpack run build 10 | -------------------------------------------------------------------------------- /src/js/modules/README.md: -------------------------------------------------------------------------------- 1 | Put all the JS code (node.js modules) and corresponding unit-tests (mocha spec 2 | files) here. `require()` these modules from the main entry-point JS files one 3 | directory up. 4 | -------------------------------------------------------------------------------- /src/css/common.css: -------------------------------------------------------------------------------- 1 | body { font-family: verdana; font-size: 12px; } 2 | label { display: block; } 3 | td { vertical-align: top; } 4 | form { width: 400px; } 5 | button { width: 100%; height: 24px; margin-top: 10px; font-size: 14px; } 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["es2015", { "modules": false }], "stage-2"], 3 | "plugins": [ 4 | "transform-runtime", 5 | ["module-resolver", { 6 | "root": ["./"], 7 | "alias": {} 8 | }] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/js/modules/circleci.node.fix.spec.js: -------------------------------------------------------------------------------- 1 | // node.js version on CircleCI does not contain setImmediate function yet, 2 | // so we need to have a workaround here... 3 | global.setImmediate = global.setImmediate || ((callback) => { setTimeout(callback, 0); }); 4 | -------------------------------------------------------------------------------- /src/html/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dev Tools 5 | 6 | 7 | 8 |
Dev Tools...
9 | 10 | 11 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | The `name`, `version` and `description` fields will be added to the 2 | `manifest.json` file from main `package.json` during the build process. The JS 3 | files referenced here and also in all the HTML files in `html` directory will 4 | see the *processed* files (all the dependencies resolved). 5 | -------------------------------------------------------------------------------- /src/js/modules/chrome.devtools.mock.js: -------------------------------------------------------------------------------- 1 | // 2 | // chrome.devtools.inspectedWindow.tabId 3 | // 4 | 5 | // return the same id the same time 6 | const data = { inspectedWindow: { tabId: 1 } }; 7 | data.__setTabId = function(id) { data.inspectedWindow.tabId = id; }; // eslint-disable-line 8 | 9 | // exported 10 | export const devtools = data; 11 | -------------------------------------------------------------------------------- /webpack/webpack.config.test.js: -------------------------------------------------------------------------------- 1 | const nodeExternals = require('webpack-node-externals'); 2 | 3 | module.exports = { 4 | target: 'node', // in order to ignore built-in modules like path, fs, etc. 5 | externals: [nodeExternals()], // in order to ignore all modules in node_modules folder 6 | module: { 7 | loaders: [{ 8 | test: /\.js$/, 9 | loader: 'babel-loader' 10 | }] 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/js/README.md: -------------------------------------------------------------------------------- 1 | All the files here are considered to be *entry points* from `manifest.json` or 2 | from any of the HTML pages in `html` directory. 3 | 4 | All files here are written as node.js modules, so they can be easily tested 5 | during build process using `mocha` test framework. 6 | 7 | In the resulting extension, however, will only see *processed* files (i.e. no 8 | `libs` or `modules` directory will be visible to the extension). 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "mocha": true 5 | }, 6 | "extends": "airbnb/base", 7 | "parser": "babel-eslint", 8 | "rules": { 9 | "comma-dangle": 0, 10 | "import/prefer-default-export": 0, 11 | 12 | "no-underscore-dangle": 0, 13 | "yoda": 0, 14 | "prefer-rest-params": 0 15 | }, 16 | "settings": { 17 | "import/resolver": { 18 | "babel-module": {} 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/js/modules/chrome.devtools.mock.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { devtools } from './chrome.devtools.mock'; 3 | 4 | describe('chrome.devtools.mock module', () => { 5 | it('should export static data structure', () => { 6 | const id = 10; 7 | assert(typeof devtools === 'object'); 8 | devtools.__setTabId(id); // eslint-disable-line 9 | assert(typeof devtools.inspectedWindow === 'object'); 10 | assert(id === devtools.inspectedWindow.tabId); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /webpack/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: { 5 | background: './src/js/background', 6 | content: './src/js/content', 7 | devTools: './src/js/devTools', 8 | options: './src/js/options', 9 | popup: './src/js/popup' 10 | }, 11 | output: { 12 | filename: './js/[name].js' 13 | }, 14 | resolve: { 15 | modules: [path.join(__dirname, 'src'), 'node_modules'] 16 | }, 17 | module: { 18 | rules: [{ 19 | test: /\.js$/, 20 | loaders: ['babel-loader'], 21 | include: path.resolve(__dirname, '../src/js') 22 | }] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "icons": { "128": "images/icon.png" }, 4 | "browser_action": { 5 | "default_icon": "images/icon.png", 6 | "default_popup": "html/popup.html" 7 | }, 8 | "background": { "scripts": ["js/background.js"] }, 9 | "content_scripts": [{ 10 | "matches": [ "http://*/*", "https://*/*" ], 11 | "js": [ "js/content.js" ] 12 | }], 13 | "options_page": "html/options.html", 14 | "devtools_page": "html/devtools.html", 15 | "permissions": [ "" ], 16 | "web_accessible_resources": [ "js/*", "html/*", "css/*", "images/*" ], 17 | "content_security_policy": "script-src 'self'; object-src 'self'", 18 | "name": "<%= package.name %>", 19 | "version": "<%= package.version %>", 20 | "description": "<%= package.description %>" 21 | } 22 | -------------------------------------------------------------------------------- /webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const path = require('path'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | const VersionFilePlugin = require('webpack-version-file-plugin'); 5 | 6 | const config = require('./config.js'); 7 | 8 | 9 | module.exports = _.merge({}, config, { 10 | output: { 11 | path: path.resolve(__dirname, '../build/dev'), 12 | }, 13 | 14 | devtool: 'source-map', 15 | plugins: [ 16 | new CopyWebpackPlugin([ 17 | { from: './src' } 18 | ], { 19 | ignore: ['js/**/*', 'manifest.json'], 20 | copyUnmodified: false 21 | }), 22 | new VersionFilePlugin({ 23 | packageFile: path.resolve(__dirname, '../package.json'), 24 | template: path.resolve(__dirname, '../src/manifest.json'), 25 | outputFile: path.resolve(__dirname, '../build/dev/manifest.json'), 26 | }) 27 | ], 28 | watch: true 29 | }); 30 | -------------------------------------------------------------------------------- /src/js/devTools.js: -------------------------------------------------------------------------------- 1 | import handlers from './modules/handlers'; 2 | import msg from './modules/msg'; 3 | 4 | // here we use SHARED message handlers, so all the contexts support the same 5 | // commands. but this is NOT typical messaging system usage, since you usually 6 | // want each context to handle different commands. for this you don't need 7 | // handlers factory as used below. simply create individual `handlers` object 8 | // for each context and pass it to msg.init() call. in case you don't need the 9 | // context to support any commands, but want the context to cooperate with the 10 | // rest of the extension via messaging system (you want to know when new 11 | // instance of given context is created / destroyed, or you want to be able to 12 | // issue command requests from this context), you may simply omit the 13 | // `handlers` parameter for good when invoking msg.init() 14 | 15 | msg.init('dt', handlers.create('dt')); 16 | -------------------------------------------------------------------------------- /src/js/modules/handlers.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import handlers from './handlers'; 3 | 4 | let h; 5 | 6 | // surpress console.log 7 | handlers.__resetLog(); // eslint-disable-line no-underscore-dangle 8 | 9 | describe('handlers module', () => { 10 | it('should export create() function', () => { 11 | assert.strictEqual(handlers && typeof handlers.create, 'function'); 12 | }); 13 | 14 | it('should create() handler object with 3 commands', () => { 15 | h = handlers.create('test'); 16 | assert(typeof h === 'object'); 17 | assert(Object.keys(h).length === 3); 18 | assert.deepEqual(['echo', 'random', 'randomAsync'], Object.keys(h).sort()); 19 | }); 20 | 21 | it('should "return" random number 0 - 999', () => { 22 | h.random((i) => { 23 | assert(typeof i === 'number'); 24 | assert(i >= 0); 25 | assert(i <= 999); 26 | }); 27 | }); 28 | 29 | // randomAsync and echo commands not tested ... nothing interesting there 30 | }); 31 | -------------------------------------------------------------------------------- /src/js/content.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import handlers from './modules/handlers'; 3 | import msg from './modules/msg'; 4 | 5 | // here we use SHARED message handlers, so all the contexts support the same 6 | // commands. but this is NOT typical messaging system usage, since you usually 7 | // want each context to handle different commands. for this you don't need 8 | // handlers factory as used below. simply create individual `handlers` object 9 | // for each context and pass it to msg.init() call. in case you don't need the 10 | // context to support any commands, but want the context to cooperate with the 11 | // rest of the extension via messaging system (you want to know when new 12 | // instance of given context is created / destroyed, or you want to be able to 13 | // issue command requests from this context), you may simply omit the 14 | // `handlers` parameter for good when invoking msg.init() 15 | 16 | console.log('CONTENT SCRIPT WORKS!'); // eslint-disable-line no-console 17 | 18 | msg.init('ct', handlers.create('ct')); 19 | 20 | console.log('jQuery version:', $().jquery); // eslint-disable-line no-console 21 | -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | import handlers from './modules/handlers'; 2 | import msg from './modules/msg'; 3 | import form from './modules/form'; 4 | import runner from './modules/runner'; 5 | 6 | // here we use SHARED message handlers, so all the contexts support the same 7 | // commands. but this is NOT typical messaging system usage, since you usually 8 | // want each context to handle different commands. for this you don't need 9 | // handlers factory as used below. simply create individual `handlers` object 10 | // for each context and pass it to msg.init() call. in case you don't need the 11 | // context to support any commands, but want the context to cooperate with the 12 | // rest of the extension via messaging system (you want to know when new 13 | // instance of given context is created / destroyed, or you want to be able to 14 | // issue command requests from this context), you may simply omit the 15 | // `handlers` parameter for good when invoking msg.init() 16 | 17 | console.log('POPUP SCRIPT WORKS!'); // eslint-disable-line no-console 18 | 19 | form.init(runner.go.bind(runner, msg.init('popup', handlers.create('popup')))); 20 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | import handlers from './modules/handlers'; 2 | import msg from './modules/msg'; 3 | import form from './modules/form'; 4 | import runner from './modules/runner'; 5 | 6 | // here we use SHARED message handlers, so all the contexts support the same 7 | // commands. but this is NOT typical messaging system usage, since you usually 8 | // want each context to handle different commands. for this you don't need 9 | // handlers factory as used below. simply create individual `handlers` object 10 | // for each context and pass it to msg.init() call. in case you don't need the 11 | // context to support any commands, but want the context to cooperate with the 12 | // rest of the extension via messaging system (you want to know when new 13 | // instance of given context is created / destroyed, or you want to be able to 14 | // issue command requests from this context), you may simply omit the 15 | // `hadnlers` parameter for good when invoking msg.init() 16 | 17 | console.log('OPTIONS SCRIPT WORKS!'); // eslint-disable-line no-console 18 | 19 | form.init(runner.go.bind(runner, msg.init('options', handlers.create('options')))); 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /src/js/modules/handlers.js: -------------------------------------------------------------------------------- 1 | // create handler module for given `context`. 2 | // handles `random`, `randomAsync`, and `echo` commands. 3 | // both `random` function log the invocation information to console and return 4 | // random number 0 - 999. `randomAsync` returns the value with 15 second delay. 5 | // `echo` function doesn't return anything, just logs the input parameter 6 | // `what`. 7 | 8 | function log(...args) { 9 | console.log(...args); // eslint-disable-line no-console 10 | } 11 | 12 | const handlers = {}; 13 | 14 | handlers.create = context => ({ 15 | random: (done) => { 16 | log(`--->${context}::random() invoked`); 17 | const r = Math.floor(1000 * Math.random()); 18 | log(`<--- returns: ${r}`); 19 | done(r); 20 | }, 21 | randomAsync: (done) => { 22 | log(`--->${context}::randomAsync() invoked (15 sec delay)`); 23 | setTimeout(() => { 24 | const r = Math.floor(1000 * Math.random()); 25 | log(`<--- returns: ${r}`); 26 | done(r); 27 | }, 15 * 1000); 28 | }, 29 | echo: (what, done) => { 30 | log(`---> ${context}::echo("${what}") invoked`); 31 | log('<--- (no return value)'); 32 | done(); 33 | } 34 | }); 35 | 36 | // for surpressing console.log output in unit tests: 37 | handlers.__resetLog = () => { // eslint-disable-line no-underscore-dangle 38 | log = () => {}; // eslint-disable-line no-func-assign 39 | }; 40 | 41 | export default handlers; 42 | -------------------------------------------------------------------------------- /webpack/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const VersionFilePlugin = require('webpack-version-file-plugin'); 6 | const CrxPlugin = require('crx-webpack-plugin'); 7 | 8 | const config = require('./config.js'); 9 | const pkg = require('../package.json'); 10 | 11 | const appName = `${pkg.name}-${pkg.version}`; 12 | 13 | 14 | module.exports = _.merge({}, config, { 15 | output: { 16 | path: path.resolve(__dirname, '../build/prod'), 17 | }, 18 | 19 | // devtool: 'eval', 20 | plugins: [ 21 | new CopyWebpackPlugin([ 22 | { from: './src' } 23 | ], { 24 | ignore: ['js/**/*', 'manifest.json'], 25 | copyUnmodified: true 26 | }), 27 | new VersionFilePlugin({ 28 | packageFile: path.resolve(__dirname, '../package.json'), 29 | template: path.resolve(__dirname, '../src/manifest.json'), 30 | outputFile: path.resolve(__dirname, '../build/prod/manifest.json'), 31 | }), 32 | new CrxPlugin({ 33 | keyFile: '../mykey.pem', 34 | contentPath: '../build/prod', 35 | outputPath: '../build', 36 | name: appName 37 | }), 38 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }), 39 | new webpack.optimize.UglifyJsPlugin({ 40 | compressor: { 41 | screw_ie8: true, 42 | warnings: false 43 | }, 44 | mangle: { 45 | screw_ie8: true 46 | }, 47 | output: { 48 | comments: false, 49 | screw_ie8: true 50 | } 51 | }), 52 | ] 53 | }); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extension-skeleton", 3 | "version": "0.3.0", 4 | "description": "Bare Google Chrome extension skeleton.", 5 | "engines": { 6 | "node": "7.0.0", 7 | "npm": "3.10.8" 8 | }, 9 | "scripts": { 10 | "build": "mkdir -p ./build/prod && webpack --config ./webpack/webpack.config.prod.js", 11 | "start": "mkdir -p ./build/dev && webpack --config ./webpack/webpack.config.dev.js", 12 | "lint": "eslint src/js/", 13 | "test": "mocha-webpack --watch --webpack-config ./webpack/webpack.config.test.js \"src/js/**/*.spec.js\"", 14 | "test:ci": "mocha-webpack --webpack-config ./webpack/webpack.config.test.js \"src/js/**/*.spec.js\"" 15 | }, 16 | "dependencies": { 17 | "jquery": "3.1.1", 18 | "lodash": "4.17.4" 19 | }, 20 | "devDependencies": { 21 | "babel-core": "6.18.2", 22 | "babel-eslint": "7.1.0", 23 | "babel-loader": "6.2.7", 24 | "babel-plugin-module-resolver": "2.3.0", 25 | "babel-plugin-transform-runtime": "6.15.0", 26 | "babel-preset-es2015": "6.18.0", 27 | "babel-preset-stage-2": "6.18.0", 28 | "copy-webpack-plugin": "4.0.1", 29 | "crx-webpack-plugin": "0.1.5", 30 | "css-loader": "0.26.0", 31 | "eslint": "3.14.0", 32 | "eslint-config-airbnb": "14.0.0", 33 | "eslint-import-resolver-babel-module": "2.2.1", 34 | "eslint-plugin-import": "2.2.0", 35 | "eventemitter2": "0.4.14", 36 | "mocha": "1.20.0", 37 | "mocha-webpack": "0.7.0", 38 | "sinon": "1.12.2", 39 | "webpack": "2.2.1", 40 | "webpack-dev-server": "1.16.2", 41 | "webpack-node-externals": "1.5.4", 42 | "webpack-version-file-plugin": "0.2.2" 43 | }, 44 | "export-symbol": "extensionSkeleton.exports" 45 | } 46 | -------------------------------------------------------------------------------- /src/js/background.js: -------------------------------------------------------------------------------- 1 | import handlers from './modules/handlers'; 2 | import msg from './modules/msg'; 3 | 4 | // here we use SHARED message handlers, so all the contexts support the same 5 | // commands. in background, we extend the handlers with two special 6 | // notification hooks. but this is NOT typical messaging system usage, since 7 | // you usually want each context to handle different commands. for this you 8 | // don't need handlers factory as used below. simply create individual 9 | // `handlers` object for each context and pass it to msg.init() call. in case 10 | // you don't need the context to support any commands, but want the context to 11 | // cooperate with the rest of the extension via messaging system (you want to 12 | // know when new instance of given context is created / destroyed, or you want 13 | // to be able to issue command requests from this context), you may simply 14 | // omit the `hadnlers` parameter for good when invoking msg.init() 15 | 16 | console.log('BACKGROUND SCRIPT WORKS!'); // eslint-disable-line no-console 17 | 18 | // adding special background notification handlers onConnect / onDisconnect 19 | function logEvent(ev, context, tabId) { 20 | console.log(`${ev}: context = ${context}, tabId = ${tabId}`); // eslint-disable-line no-console 21 | } 22 | handlers.onConnect = logEvent.bind(null, 'onConnect'); 23 | handlers.onDisconnect = logEvent.bind(null, 'onDisconnect'); 24 | const message = msg.init('bg', handlers.create('bg')); 25 | 26 | // issue `echo` command in 10 seconds after invoked, 27 | // schedule next run in 5 minutes 28 | function helloWorld() { 29 | console.log('===== will broadcast "hello world!" in 10 seconds'); // eslint-disable-line no-console 30 | setTimeout(() => { 31 | console.log('>>>>> broadcasting "hello world!" now'); // eslint-disable-line no-console 32 | message.bcast('echo', 'hello world!', () => 33 | console.log('<<<<< broadcasting done') // eslint-disable-line no-console 34 | ); 35 | }, 10 * 1000); 36 | setTimeout(helloWorld, 5 * 60 * 1000); 37 | } 38 | 39 | // start broadcasting loop 40 | helloWorld(); 41 | -------------------------------------------------------------------------------- /src/js/modules/runner.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import sinon from 'sinon'; // eslint-disable-line import/no-extraneous-dependencies 3 | 4 | import runner from './runner'; 5 | import message from './msg'; 6 | 7 | // surpress console.log 8 | runner.__resetLog(); 9 | 10 | let msg; 11 | 12 | describe('runner module', () => { 13 | beforeEach(() => { 14 | msg = { 15 | bcast: sinon.spy(), 16 | cmd: sinon.spy(), 17 | bg: sinon.spy() 18 | }; 19 | }); 20 | 21 | it('should export go() function', () => { 22 | assert.strictEqual(runner && typeof runner.go, 'function'); 23 | }); 24 | 25 | it('should invoke msg.bg() function', () => { 26 | runner.go(msg, { type: 'bg', cmd: 'echo' }); 27 | assert(!msg.bcast.calledOnce); 28 | assert(!msg.cmd.calledOnce); 29 | assert(msg.bg.calledOnce); 30 | }); 31 | 32 | it('should invoke msg.cmd() function', () => { 33 | runner.go(msg, { type: 'cmd', cmd: 'echo' }); 34 | assert(!msg.bcast.calledOnce); 35 | assert(msg.cmd.calledOnce); 36 | assert(!msg.bg.calledOnce); 37 | }); 38 | 39 | it('should invoke msg.bcast() function', () => { 40 | runner.go(msg, { type: 'bcast', cmd: 'echo' }); 41 | assert(msg.bcast.calledOnce); 42 | assert(!msg.cmd.calledOnce); 43 | assert(!msg.bg.calledOnce); 44 | }); 45 | 46 | it('should issue "echo" command with provided argument', () => { 47 | runner.go(msg, { type: 'bcast', cmd: 'echo', arg: 'hello', tab: -1, ctx_all: true }); 48 | assert(msg.bcast.calledWith('echo', 'hello')); 49 | }); 50 | 51 | it('should issue "random" command and ignore provided argument', () => { 52 | runner.go(msg, { type: 'bcast', cmd: 'random', arg: 'hello', tab: -1, ctx_all: true }); 53 | assert(!msg.bcast.calledWith('random', 'hello')); 54 | }); 55 | 56 | it('should issue "echo" command with tabId = SAME_TAB', () => { 57 | runner.go(msg, { type: 'bcast', cmd: 'echo', arg: 'hello', tab: -2, ctx_all: true }); 58 | assert(msg.bcast.calledWith(message.SAME_TAB, 'echo', 'hello')); 59 | }); 60 | 61 | it('should issue "echo" command with provided tabId', () => { 62 | runner.go(msg, { type: 'bcast', cmd: 'echo', arg: 'hello', tab: 42, ctx_all: true }); 63 | assert(msg.bcast.calledWith(42, 'echo', 'hello')); 64 | }); 65 | 66 | it('should issue "echo" command with provided contexts', () => { 67 | runner.go(msg, { type: 'bcast', cmd: 'echo', arg: 'hello', tab: -1, ctx_all: false, ctxs: ['ctx1', 'ctx2', 'ctx3'] }); 68 | assert(msg.bcast.calledWith(['ctx1', 'ctx2', 'ctx3'], 'echo', 'hello')); 69 | }); 70 | 71 | it('should issue "echo" command with provided tabId and contexts', () => { 72 | runner.go(msg, { type: 'bcast', cmd: 'echo', arg: 'hello', tab: 42, ctx_all: false, ctxs: ['ctx1', 'ctx2', 'ctx3'] }); 73 | assert(msg.bcast.calledWith(42, ['ctx1', 'ctx2', 'ctx3'], 'echo', 'hello')); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/js/modules/chrome0.runtime: -------------------------------------------------------------------------------- 1 | // event emitter for message passing 2 | var EE2 = require('eventemitter2').EventEmitter2; 3 | 4 | // event factory 5 | function createEvent(bus, name) { 6 | return { 7 | addListener: function(callback) { bus.on(name, callback); }, 8 | removeListener: function(callback) { bus.removeListener(name, callback); } 9 | }; 10 | } 11 | 12 | var runtimes = {}; 13 | 14 | function ChromeRuntime() { 15 | 16 | // tab id is increased every second .connect() 17 | var counter = 0; 18 | var tabId = 1; 19 | 20 | // 21 | // chrome.runtime.Port 22 | // 23 | 24 | function Port(id, info, bus, myPrefix, otherPrefix) { 25 | // public properties 26 | this.name = info && info.name; 27 | if ('A' === myPrefix) { // on that will be passed to onConnect 28 | this.sender = { id: id, tab: { id: tabId } }; 29 | if (counter++ & 1) { tabId++; } 30 | if ('dt' === this.name) { this.sender = {}; } // developer tools 31 | } 32 | // disconnect 33 | this.disconnect = function() { 34 | setImmediate(bus.emit.bind(bus, otherPrefix + 'disconnect')); 35 | }; 36 | this.onDisconnect = createEvent(bus, myPrefix + 'disconnect'); 37 | // postMessage 38 | this.postMessage = function(msg) { 39 | // msg should be serializable (so that it can be passed accross process 40 | // boundaries). we create a deep copy of it using JSON, so that we know that 41 | // the message we pass is unique in each context we pass it into (even if 42 | // we send the same message (or with the same deep references) over and over 43 | // again to multiple destinations). 44 | var _str = JSON.stringify({ data: msg }); 45 | var _obj = JSON.parse(_str); 46 | setImmediate(bus.emit.bind(bus, otherPrefix + 'message', _obj.data)); 47 | }; 48 | this.onMessage = createEvent(bus, myPrefix + 'message'); 49 | } 50 | 51 | 52 | // event dispatcher for chrome.runtime 53 | var server = new EE2(); 54 | 55 | // 56 | // chrome.runtime.(connect/onConnect): 57 | // 58 | 59 | this.id = Math.floor(Math.random() * 10000000).toString(); 60 | runtimes[this.id] = function(port) { // store connect invoker for external connections 61 | setImmediate(server.emit.bind(server, 'connectExternal', port)); 62 | }; 63 | 64 | this.onConnect = createEvent(server, 'connect'); 65 | this.onConnectExternal = createEvent(server, 'connectExternal'); 66 | 67 | this.connect = function() { 68 | // process args: 69 | var id = arguments[0], info = arguments[0]; 70 | if (typeof(id) === 'string') { info = arguments[1]; } // id provided 71 | else { id = undefined; } // id not provided 72 | // shared event bus for two communicating Ports 73 | var bus = new EE2(); 74 | var portA = new Port(this.id, info, bus, 'A', 'B'); 75 | var portB = new Port(id, info, bus, 'B', 'A'); 76 | // let the port register onMessage --> setImmediate() 77 | if (id) { 78 | if (typeof(runtimes[id]) === 'function') { 79 | runtimes[id](portA); 80 | } 81 | } 82 | else { 83 | setImmediate(server.emit.bind(server, 'connect', portA)); 84 | } 85 | return portB; 86 | }; 87 | 88 | // for unit tests only 89 | this.__resetTabId = function(val) { 90 | tabId = val || 1; 91 | counter = 0; 92 | }; 93 | } 94 | 95 | 96 | // exported 97 | module.exports = ChromeRuntime; 98 | -------------------------------------------------------------------------------- /src/js/modules/chrome.runtime.mock.js: -------------------------------------------------------------------------------- 1 | // event emitter for message passing 2 | import { EventEmitter2 } from 'eventemitter2'; // eslint-disable-line import/no-extraneous-dependencies 3 | 4 | 5 | // event factory 6 | function createEvent(bus, name) { 7 | return { 8 | addListener(callback) { bus.on(name, callback); }, 9 | removeListener(callback) { bus.removeListener(name, callback); } 10 | }; 11 | } 12 | 13 | const runtimes = {}; 14 | 15 | function ChromeRuntime() { 16 | // tab id is increased every second .connect() 17 | let counter = 0; 18 | let tabId = 1; 19 | 20 | // 21 | // chrome.runtime.Port 22 | // 23 | 24 | function Port(id, info, bus, myPrefix, otherPrefix) { 25 | // public properties 26 | this.name = info && info.name; 27 | if (myPrefix === 'A') { // on that will be passed to onConnect 28 | this.sender = { id, tab: { id: tabId } }; 29 | if (counter++ & 1) { tabId++; } // eslint-disable-line 30 | if (this.name === 'dt') { this.sender = {}; } // developer tools 31 | } 32 | // disconnect 33 | this.disconnect = () => { 34 | setImmediate(bus.emit.bind(bus, `${otherPrefix}disconnect`)); 35 | }; 36 | this.onDisconnect = createEvent(bus, `${myPrefix}disconnect`); 37 | // postMessage 38 | this.postMessage = (msg) => { 39 | // msg should be serializable (so that it can be passed accross process 40 | // boundaries). we create a deep copy of it using JSON, so that we know that 41 | // the message we pass is unique in each context we pass it into (even if 42 | // we send the same message (or with the same deep references) over and over 43 | // again to multiple destinations). 44 | const _str = JSON.stringify({ data: msg }); 45 | const _obj = JSON.parse(_str); 46 | setImmediate(bus.emit.bind(bus, `${otherPrefix}message`, _obj.data)); 47 | }; 48 | this.onMessage = createEvent(bus, `${myPrefix}message`); 49 | } 50 | 51 | 52 | // event dispatcher for chrome.runtime 53 | const server = new EventEmitter2(); 54 | 55 | // 56 | // chrome.runtime.(connect/onConnect): 57 | // 58 | 59 | this.id = Math.floor(Math.random() * 10000000).toString(); 60 | runtimes[this.id] = (port) => { // store connect invoker for external connections 61 | setImmediate(server.emit.bind(server, 'connectExternal', port)); 62 | }; 63 | 64 | this.onConnect = createEvent(server, 'connect'); 65 | this.onConnectExternal = createEvent(server, 'connectExternal'); 66 | 67 | this.connect = (...args) => { 68 | // process args: 69 | let id = args[0]; 70 | let info = args[0]; 71 | if (typeof id === 'string') { // id provided 72 | info = args[1]; 73 | } else { // id not provided 74 | id = undefined; 75 | } 76 | // shared event bus for two communicating Ports 77 | const bus = new EventEmitter2(); 78 | const portA = new Port(this.id, info, bus, 'A', 'B'); 79 | const portB = new Port(id, info, bus, 'B', 'A'); 80 | // let the port register onMessage --> setImmediate() 81 | if (id) { 82 | if (typeof runtimes[id] === 'function') { 83 | runtimes[id](portA); 84 | } 85 | } else { 86 | setImmediate(server.emit.bind(server, 'connect', portA)); 87 | } 88 | return portB; 89 | }; 90 | 91 | // for unit tests only 92 | this.__resetTabId = (val) => { // eslint-disable-line no-underscore-dangle 93 | tabId = val || 1; 94 | counter = 0; 95 | }; 96 | } 97 | 98 | 99 | // exported 100 | export default ChromeRuntime; 101 | -------------------------------------------------------------------------------- /src/js/modules/runner.js: -------------------------------------------------------------------------------- 1 | // module that translates structured information about command invocation from 2 | // form.js into real command invocation 3 | // 4 | // exported function `go` takes two parameters: messaging object `msg` on which 5 | // it'll invoke the methods, and structured `info` which is collected from the 6 | // form. we assume both `msg` and `info` parameters to be valid 7 | 8 | import message from './msg'; 9 | 10 | let log = console.log.bind(console); // eslint-disable-line no-console 11 | function callback(res) { log(`<<<<< callback invoked, return value = ${JSON.stringify(res)}`); } 12 | 13 | const runner = {}; 14 | 15 | runner.go = (msg, info) => { 16 | if ('bg' === info.type) { // msg.bg 17 | if ('echo' === info.cmd) { 18 | log(`>>>>> invoking msg.bg('echo', '${info.arg}', callback)`); 19 | msg.bg('echo', info.arg, callback); 20 | } else if ('random' === info.cmd) { 21 | log(">>>>> invoking msg.bg('random', callback)"); 22 | msg.bg('random', callback); 23 | } else { 24 | log(">>>>> invoking msg.bg('randomAsync', callback) ... 15 sec delay"); 25 | msg.bg('randomAsync', callback); 26 | } 27 | } else if ('echo' === info.cmd) { // msg.bcast + msg.cmd 28 | if (-1 === info.tab) { // all tab ids 29 | if (info.ctx_all) { 30 | log(`>>>>> invoking msg.${info.type}('echo', '${info.arg}', callback)`); 31 | msg[info.type]('echo', info.arg, callback); 32 | } else { 33 | log(`>>>>> invoking msg.${info.type}(${JSON.stringify(info.ctxs)}, 'echo', '${info.arg}', callback)`); 34 | msg[info.type](info.ctxs, 'echo', info.arg, callback); 35 | } 36 | } else if (-2 === info.tab) { // same id 37 | if (info.ctx_all) { 38 | log(`>>>>> invoking msg.${info.type}(SAME_TAB, 'echo', '${info.arg}', callback)`); 39 | msg[info.type](message.SAME_TAB, 'echo', info.arg, callback); 40 | } else { 41 | log(`>>>>> invoking msg.${info.type}(SAME_TAB, ${JSON.stringify(info.ctxs)}, 'echo', '${info.arg}', callback)`); 42 | msg[info.type](message.SAME_TAB, info.ctxs, 'echo', info.arg, callback); 43 | } 44 | } else if (info.ctx_all) { // tab id provided 45 | log(`>>>>> invoking msg.${info.type}(${info.tab}, 'echo', '${info.arg}', callback)`); 46 | msg[info.type](info.tab, 'echo', info.arg, callback); 47 | } else { 48 | log(`>>>>> invoking msg.${info.type}(${info.tab}, ${JSON.stringify(info.ctxs)}, 'echo', '${info.arg}', callback)`); 49 | msg[info.type](info.tab, info.ctxs, 'echo', info.arg, callback); 50 | } 51 | } else if (-1 === info.tab) { // all tab ids // random + randomAsync 52 | if (info.ctx_all) { 53 | log(`>>>>> invoking msg.${info.type}('${info.cmd}', callback)`); 54 | msg[info.type](info.cmd, callback); 55 | } else { 56 | log(`>>>>> invoking msg.${info.type}(${JSON.stringify(info.ctxs)}, '${info.cmd}', callback)`); 57 | msg[info.type](info.ctxs, info.cmd, callback); 58 | } 59 | } else if (-2 === info.tab) { // same id 60 | if (info.ctx_all) { 61 | log(`>>>>> invoking msg.${info.type}(SAME_TAB, '${info.cmd}', callback)`); 62 | msg[info.type](message.SAME_TAB, info.cmd, callback); 63 | } else { 64 | log(`>>>>> invoking msg.${info.type}(SAME_TAB, ${JSON.stringify(info.ctxs)}, '${info.cmd}', callback)`); 65 | msg[info.type](message.SAME_TAB, info.ctxs, info.cmd, callback); 66 | } 67 | } else if (info.ctx_all) { // tab id provided 68 | log(`>>>>> invoking msg.${info.type}(${info.tab}, '${info.cmd}', callback)`); 69 | msg[info.type](info.tab, info.cmd, callback); 70 | } else { 71 | log(`>>>>> invoking msg.${info.type}(${info.tab}, ${JSON.stringify(info.ctxs)}, '${info.cmd}', callback)`); 72 | msg[info.type](info.tab, info.ctxs, info.cmd, callback); 73 | } 74 | }; 75 | 76 | // for surpressing console.log output in unit tests: 77 | runner.__resetLog = () => { log = () => {}; }; 78 | 79 | export default runner; 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Skeleton for Google Chrome extensions 2 | 3 | * includes awesome messaging module 4 | * webpack-based build system 5 | * full ES6 support with Babel 6 6 | * linting using eslint with airbnb configuration 7 | * use node.js libraries 8 | * unit-tests in mocha 9 | * CircleCI friendly 10 | 11 | ### Installation: 12 | 13 | git clone git@github.com:salsita/chrome-extension-skeleton.git 14 | 15 | # in case you don't have webpack yet: 16 | sudo npm install -g webpack 17 | 18 | ### Build instructions: 19 | 20 | To install dependencies: 21 | 22 | cd chrome-extension-skeleton 23 | npm install 24 | 25 | Then to start a developing session (with watch), run: 26 | 27 | npm start 28 | 29 | To start a unit testing session (with watch): 30 | 31 | npm test 32 | 33 | To check code for linting errors: 34 | 35 | npm run lint 36 | 37 | 38 | To build production code + crx: 39 | 40 | npm run build 41 | 42 | To run unit tests in CI scripts: 43 | 44 | npm run test:ci 45 | 46 | 47 | ### Directory structure: 48 | 49 | /build # this is where your extension (.crx) will end up, 50 | # along with unpacked directories of production and 51 | # develop build (for debugging) 52 | 53 | /src 54 | /css # CSS files 55 | /html # HTML files 56 | /images # image resources 57 | 58 | /js # entry-points for browserify, requiring node.js `modules` 59 | 60 | /libs # 3rd party run-time libraries, excluded from JS-linting 61 | /modules # node.js modules (and corresponding mocha 62 | # unit tests spec files) 63 | 64 | manifest.json # skeleton manifest file, `name`, `description` 65 | # and `version` fields copied from `package.json` 66 | 67 | /webpack # webpack configuration files 68 | 69 | .babelrc # Babel configuration 70 | .eslintrc # options for JS-linting 71 | circle.yml # integration with CircleCI 72 | mykey.pem # certificate file, YOU NEED TO GENERATE THIS FILE, see below 73 | package.json # project description file (name, version, dependencies, ...) 74 | 75 | 76 | ### After you clone: 77 | 78 | 1. In `package.json`, rename the project, description, version, add dependencies 79 | and any other fields necessary. 80 | 81 | 2. Generate your .pem key and store it in the root as `mykey.pem` file. On 82 | unix / mac, the command to generate the file is 83 | `openssl genrsa 2048 | openssl pkcs8 -topk8 -nocrypt > mykey.pem`. 84 | Note: the generated file is in `.gitignore` file, it won't be (and should NOT 85 | be) commited to the repository unless you know what you are doing. 86 | 87 | 3. Add content (HTML, CSS, images, JS modules), update `code/manifest.json`, 88 | leave only JS entry-points you use (remove the ones you don't need). 89 | 90 | 4. When developing, write unit-tests, use `npm test` to check that 91 | your code passes unit-tests and `npm run lint` to check for linting errors. 92 | 93 | 5. When ready to try out the extension in the browser, use `npm start` to 94 | build it. In `build` directory you'll find develop version of the extension in 95 | `dev` subdirectory (with source maps), and production (uglified) 96 | version in `prod` directory. The `.crx` packed version is created from 97 | `prod` sources. 98 | 99 | 6. When done developing, publish the extension and enjoy it (profit!). 100 | 101 | Use any 3rd party libraries you need (both for run-time and for development / 102 | testing), use regular npm node.js modules (that will be installed into 103 | `node_modules` directory). These libraries will be encapsulated in the resulting 104 | code and will NOT conflict even with libraries on pages where you inject the 105 | resulting JS scripts to (for content scripts). 106 | 107 | For more information, please check also README.md files in subdirectories. 108 | 109 | ### Under the hood: 110 | 111 | If you want to understand better the structure of the code and how it really 112 | works, please check the following (note: somewhat out of date, with respect to the build system and ES6): 113 | 114 | * [Prezi presentation](http://prezi.com/yxj7zs7ixlmw/chrome-extension-skeleton/). 115 | -------------------------------------------------------------------------------- /src/html/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup 5 | 6 | 7 | 8 | 9 |
10 |

Popup

11 | 12 |
13 | command type 14 | 15 | 16 | 22 | 28 | 34 | 35 |
17 | 21 | 23 | 27 | 29 | 33 |
36 |
37 | 38 |
39 | command 40 | 41 | 42 | 48 | 51 | 52 | 53 | 59 | 69 | 70 |
43 | 47 | 49 | 50 |
54 | 58 | 60 | 64 | 68 |
71 |
72 | 73 |
74 | contexts 75 | 76 | 77 | 83 | 84 | 85 | 91 | 100 | 101 |
78 | 82 |
86 | 90 | 92 |
93 | 94 | 95 | 96 | 97 | 98 |
99 |
102 |
103 | 104 |
105 | tab id 106 | 107 | 108 | 114 | 115 | 116 | 122 | 123 | 124 | 130 | 133 | 134 |
109 | 113 |
117 | 121 |
125 | 129 | 131 | 132 |
135 |
136 | 137 |
138 | 139 |
140 | 141 |
142 | 143 | 144 | -------------------------------------------------------------------------------- /src/html/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Options 5 | 6 | 7 | 8 | 9 |
10 |

Options page

11 | 12 |
13 | command type 14 | 15 | 16 | 22 | 28 | 34 | 35 |
17 | 21 | 23 | 27 | 29 | 33 |
36 |
37 | 38 |
39 | command 40 | 41 | 42 | 48 | 51 | 52 | 53 | 59 | 69 | 70 |
43 | 47 | 49 | 50 |
54 | 58 | 60 | 64 | 68 |
71 |
72 | 73 |
74 | contexts 75 | 76 | 77 | 83 | 84 | 85 | 91 | 100 | 101 |
78 | 82 |
86 | 90 | 92 |
93 | 94 | 95 | 96 | 97 | 98 |
99 |
102 |
103 | 104 |
105 | tab id 106 | 107 | 108 | 114 | 115 | 116 | 122 | 123 | 124 | 130 | 133 | 134 |
109 | 113 |
117 | 121 |
125 | 129 | 131 | 132 |
135 |
136 | 137 |
138 | 139 |
140 | 141 |
142 | 143 | 144 | -------------------------------------------------------------------------------- /src/js/modules/form.js: -------------------------------------------------------------------------------- 1 | // module for manipulating / validating the form shared between options and 2 | // popup views. when 'Go!' button is pressed, structured info is passed to 3 | // provided callback. 4 | // 5 | // no unit tests for this module, it is jQuery manipulation mostly. 6 | // 7 | import $ from 'jquery'; 8 | 9 | const form = {}; 10 | 11 | form.init = (callback) => { 12 | $(() => { 13 | // form logic: 14 | $('#type_bcast, #type_cmd, #type_bg').change(() => { 15 | const bgSel = $('#type_bg').is(':checked'); 16 | $('#ctx, #tab').prop('disabled', bgSel); 17 | }); 18 | 19 | $('#cmd_echo, #cmd_random').change(() => { 20 | const echoSel = $('#cmd_echo').is(':checked'); 21 | $('#cmd_echo_text').prop('disabled', !echoSel); 22 | $('#cmd_random_sync, #cmd_random_async').prop('disabled', echoSel); 23 | }); 24 | 25 | $('#ctx_all, #ctx_select').change(() => { 26 | const ctxAll = $('#ctx_all').is(':checked'); 27 | $('input[type=checkbox]').prop('disabled', ctxAll); 28 | }); 29 | 30 | $('#tab_all, #tab_same, #tab_provided').change(() => { 31 | const tabProv = $('#tab_provided').is(':checked'); 32 | $('#tab_provided_text').prop('disabled', !tabProv); 33 | }); 34 | 35 | function validateTabId() { 36 | const el = $('#tab_provided_text'); 37 | if (el.val() === '') { el.val(1); } 38 | if (parseInt(el.val(), 10) < 0) { el.val(1); } 39 | } 40 | 41 | $('#tab_provided_text').blur(validateTabId); 42 | 43 | // button logic: 44 | $('#submit').click(() => { 45 | validateTabId(); 46 | if (typeof callback === 'function') { 47 | const res = {}; 48 | const typeBcast = $('#type_bcast').is(':checked'); 49 | const typeBg = $('#type_bg').is(':checked'); 50 | const cmdEcho = $('#cmd_echo').is(':checked'); 51 | const cmdEchoText = $('#cmd_echo_text').val(); 52 | const cmdRandomSync = $('#cmd_random_sync').is(':checked'); 53 | const ctxAll = $('#ctx_all').is(':checked'); 54 | const ctxSelBg = $('#ctx_select_bg').is(':checked'); 55 | const ctxSelCt = $('#ctx_select_ct').is(':checked'); 56 | const ctxSelDt = $('#ctx_select_dt').is(':checked'); 57 | const ctxSelPopup = $('#ctx_select_popup').is(':checked'); 58 | const ctxSelOptions = $('#ctx_select_options').is(':checked'); 59 | const tabAll = $('#tab_all').is(':checked'); 60 | const tabProvided = $('#tab_provided').is(':checked'); 61 | const tabProvidedVal = parseInt($('#tab_provided_text').val(), 10); 62 | // command: 63 | if (cmdEcho) { 64 | res.cmd = 'echo'; res.arg = cmdEchoText; 65 | } else if (cmdRandomSync) { 66 | res.cmd = 'random'; 67 | } else { 68 | res.cmd = 'randomAsync'; 69 | } 70 | // type: 71 | if (typeBg) { 72 | res.type = 'bg'; 73 | } else { 74 | if (typeBcast) { 75 | res.type = 'bcast'; 76 | } else { 77 | res.type = 'cmd'; 78 | } 79 | // contexts: 80 | res.ctx_all = ctxAll; 81 | if (!ctxAll) { 82 | const arr = []; 83 | if (ctxSelBg) { arr.push('bg'); } 84 | if (ctxSelCt) { arr.push('ct'); } 85 | if (ctxSelDt) { arr.push('dt'); } 86 | if (ctxSelPopup) { arr.push('popup'); } 87 | if (ctxSelOptions) { arr.push('options'); } 88 | res.ctxs = arr; 89 | } 90 | // tab id: 91 | if (tabAll) { 92 | res.tab = -1; 93 | } else if (tabProvided) { 94 | res.tab = tabProvidedVal; 95 | } else { 96 | res.tab = -2; 97 | } // same id 98 | } 99 | callback(res); 100 | } 101 | return false; // stop propagation 102 | }); 103 | 104 | // default values: 105 | $('#type_bcast, #cmd_random, #cmd_random_sync, #ctx_all, #tab_all').attr('checked', true); 106 | $('#cmd_echo_text').val('salsita'); 107 | $('#cmd_echo_text').prop('disabled', true); 108 | $('input[type=checkbox]').prop('checked', true); 109 | $('input[type=checkbox]').prop('disabled', true); 110 | $('#tab_provided_text').val(1); 111 | $('#tab_provided_text').prop('disabled', true); 112 | }); 113 | }; 114 | 115 | export default form; 116 | -------------------------------------------------------------------------------- /src/js/modules/chrome.runtime.mock.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import ChromeRuntime from './chrome.runtime.mock'; 3 | 4 | const runtime = new ChromeRuntime(); 5 | const runtimeExt = new ChromeRuntime(); 6 | 7 | let log = []; 8 | 9 | // function dumpLog() { console.log(JSON.stringify(log, null, 4)); } 10 | function addLogEntry(args) { 11 | log.push(args); 12 | } 13 | function createCb(scope) { 14 | return (...args) => { 15 | Array.prototype.unshift.call(args, scope); 16 | addLogEntry(args); 17 | }; 18 | } 19 | 20 | const onConnect = createCb('main::onConnect'); 21 | const onConnectExt = createCb('main::onConnectExternal'); 22 | 23 | function verifyPort(port) { 24 | assert(typeof port === 'object'); 25 | assert(port.name === 'myPort'); 26 | assert(typeof port.disconnect === 'function'); 27 | assert(typeof port.postMessage === 'function'); 28 | assert(typeof port.onDisconnect === 'object'); 29 | assert(typeof port.onDisconnect.addListener === 'function'); 30 | assert(typeof port.onDisconnect.removeListener === 'function'); 31 | assert(typeof port.onMessage === 'object'); 32 | assert(typeof port.onMessage.addListener === 'function'); 33 | assert(typeof port.onMessage.removeListener === 'function'); 34 | } 35 | 36 | describe('chrome.runtime.mock module', () => { 37 | beforeEach(() => { 38 | log = []; 39 | runtime.onConnect.addListener(onConnect); 40 | runtimeExt.onConnectExternal.addListener(onConnectExt); 41 | }); 42 | 43 | afterEach(() => { 44 | runtime.onConnect.removeListener(onConnect); 45 | runtimeExt.onConnectExternal.removeListener(onConnectExt); 46 | }); 47 | 48 | it('should export connect method and onConnect / onConnectExternal events', () => { 49 | assert(typeof runtime === 'object'); 50 | assert(typeof runtime.connect === 'function'); 51 | assert(typeof runtime.onConnect === 'object'); 52 | assert(typeof runtime.onConnect.addListener === 'function'); 53 | assert(typeof runtime.onConnect.removeListener === 'function'); 54 | assert(typeof runtime.onConnectExternal === 'object'); 55 | assert(typeof runtime.onConnectExternal.addListener === 'function'); 56 | assert(typeof runtime.onConnectExternal.removeListener === 'function'); 57 | }); 58 | 59 | it('connect() should create Port', (done) => { 60 | const port = runtime.connect({ name: 'myPort' }); 61 | verifyPort(port); 62 | assert(typeof port.sender === 'undefined'); 63 | setImmediate(done); // connect writes to log asynchronously, so need to wait here 64 | }); 65 | 66 | it('should notify onConnect handler when Port is connected', (done) => { 67 | runtime.connect({ name: 'myPort' }); 68 | setImmediate(() => { 69 | assert(log.length === 1); 70 | assert(log[0][0] === 'main::onConnect'); 71 | const port = log[0][1]; 72 | verifyPort(port); 73 | assert(typeof (port.sender && port.sender.tab && port.sender.tab.id) === 'number'); 74 | done(); 75 | }); 76 | }); 77 | 78 | it('should notify onConnectExternal handler when connect has been called with extension id', (done) => { 79 | runtime.connect(runtimeExt.id, { name: 'myPort' }); 80 | setImmediate(() => { 81 | assert(log.length === 1); 82 | assert.strictEqual(log[0][0], 'main::onConnectExternal'); 83 | const port = log[0][1]; 84 | verifyPort(port); 85 | assert.strictEqual(port.sender && port.sender.id, runtime.id); 86 | done(); 87 | }); 88 | }); 89 | 90 | it('should be able to add/remove onConnect listeners', (done) => { 91 | runtime.connect(); 92 | setImmediate(() => { 93 | assert(log.length === 1); // orig 94 | const cb = createCb('extra::onConnect'); 95 | runtime.onConnect.addListener(cb); 96 | runtime.connect(); 97 | setImmediate(() => { 98 | assert(log.length === 3); // orig + (orig + extra) 99 | assert(log[1][1] === log[2][1]); // the listners should get the same Port 100 | runtime.onConnect.removeListener(cb); 101 | runtime.connect(); 102 | setImmediate(() => { 103 | assert(log.length === 4); // orig + (orig + extra) + orig 104 | assert(log[3][0] === 'main::onConnect'); 105 | done(); 106 | }); 107 | }); 108 | }); 109 | }); 110 | 111 | it('should pass messages between Port parts', (done) => { 112 | const portA = runtime.connect(); 113 | setImmediate(() => { 114 | const portB = log[0][1]; // counterpart to portA 115 | const onMsgA = createCb('A::onMsg'); 116 | const onMsgB = createCb('B::onMsg'); 117 | portA.onMessage.addListener(onMsgA); 118 | portB.onMessage.addListener(onMsgB); 119 | portA.postMessage(); 120 | setImmediate(() => { 121 | assert(log.length === 2); 122 | assert(log[1][0] === 'B::onMsg'); 123 | portB.postMessage({ b: false, i: 1, s: 'str', a: ['a', 'b'], o: { x: 1, y: 2 } }); 124 | setImmediate(() => { 125 | assert(log.length === 3); 126 | const _ref = log[2]; // eslint-disable-line no-underscore-dangle 127 | assert(_ref[0] === 'A::onMsg'); 128 | assert.deepEqual(_ref[1], { b: false, i: 1, s: 'str', a: ['a', 'b'], o: { x: 1, y: 2 } }); 129 | done(); 130 | }); 131 | }); 132 | }); 133 | }); 134 | 135 | it('should be abble to add/remove more onMessage Port handlers', (done) => { 136 | const portA = runtime.connect(); 137 | setImmediate(() => { 138 | const portB = log[0][1]; 139 | portB.postMessage(); 140 | setImmediate(() => { 141 | assert(log.length === 1); // i.e. no message, no handler added yet 142 | const cb1 = createCb('A1::onMsg'); 143 | const cb2 = createCb('A2::onMsg'); 144 | portA.onMessage.addListener(cb1); 145 | portB.postMessage(); 146 | setImmediate(() => { 147 | assert(log.length === 2); // 1 new entry 148 | assert(log[1][0] === 'A1::onMsg'); 149 | portA.onMessage.addListener(cb2); 150 | portB.postMessage(); 151 | setImmediate(() => { 152 | assert(log.length === 4); // 2 new entries 153 | assert(log[2][0] !== log[3][0]); // coming from different handlers 154 | portA.onMessage.removeListener(cb1); 155 | portB.postMessage(); 156 | setImmediate(() => { 157 | assert(log.length === 5); 158 | assert(log[4][0] === 'A2::onMsg'); 159 | portA.onMessage.removeListener(cb1); // removing for second time, should do no harm 160 | portB.postMessage(); 161 | setImmediate(() => { 162 | assert(log.length === 6); 163 | assert(log[5][0] === 'A2::onMsg'); 164 | portA.onMessage.removeListener(cb2); 165 | portB.postMessage(); 166 | setImmediate(() => { 167 | assert(log.length === 6); // no change 168 | done(); 169 | }); 170 | }); 171 | }); 172 | }); 173 | }); 174 | }); 175 | }); 176 | }); 177 | 178 | it('should not mix msg communication across different Ports', (done) => { 179 | const port1A = runtime.connect(); 180 | const port2A = runtime.connect(); 181 | port1A.onMessage.addListener(createCb('1A::onMsg')); 182 | port2A.onMessage.addListener(createCb('2A::onMsg')); 183 | setImmediate(() => { 184 | const port1B = log[0][1]; 185 | const port2B = log[1][1]; 186 | port1B.onMessage.addListener(createCb('1B::onMsg')); 187 | port2B.onMessage.addListener(createCb('2B::onMsg')); 188 | port1A.postMessage(); 189 | setImmediate(() => { 190 | assert(log[2][0] === '1B::onMsg'); 191 | port1B.postMessage(); 192 | setImmediate(() => { 193 | assert(log[3][0] === '1A::onMsg'); 194 | port2A.postMessage(); 195 | setImmediate(() => { 196 | assert(log[4][0] === '2B::onMsg'); 197 | port2B.postMessage(); 198 | setImmediate(() => { 199 | assert(log[5][0] === '2A::onMsg'); 200 | done(); 201 | }); 202 | }); 203 | }); 204 | }); 205 | }); 206 | }); 207 | 208 | it('should notify onDisconnect handler when Port is closed', (done) => { 209 | const portA = runtime.connect(); 210 | setImmediate(() => { 211 | const portB = log[0][1]; 212 | portA.onDisconnect.addListener(createCb('A::onDisconnect')); 213 | portB.onDisconnect.addListener(createCb('B::onDisconnect')); 214 | portA.disconnect(); 215 | setImmediate(() => { 216 | assert(log.length === 2); 217 | assert(log[1][0] === 'B::onDisconnect'); 218 | portB.disconnect(); 219 | setImmediate(() => { 220 | assert(log.length === 3); 221 | assert(log[2][0] === 'A::onDisconnect'); 222 | const extraCb = createCb('A2::onDisconnect'); 223 | portA.onDisconnect.addListener(extraCb); 224 | portA.disconnect(); 225 | setImmediate(() => { 226 | assert(log.length === 4); 227 | assert(log[3][0] === 'B::onDisconnect'); 228 | portB.disconnect(); 229 | setImmediate(() => { 230 | assert(log.length === 6); 231 | assert(((log[4][0] === 'A::onDisconnect') && (log[5][0] === 'A2::onDisconnect')) || 232 | ((log[5][0] === 'A::onDisconnect') && (log[4][0] === 'A2::onDisconnect'))); 233 | portA.onDisconnect.removeListener(extraCb); 234 | portB.disconnect(); 235 | setImmediate(() => { 236 | assert(log.length === 7); 237 | assert(log[6][0] === 'A::onDisconnect'); 238 | done(); 239 | }); 240 | }); 241 | }); 242 | }); 243 | }); 244 | }); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /src/js/modules/msg.js: -------------------------------------------------------------------------------- 1 | // 2 | // Extension messaging system. 3 | // 4 | // 5 | // This module, when used, allows communication among any extension-related 6 | // contexts (background script, content scripts, development tools scripts, any 7 | // JS code running in extension-related HTML pages, such as popups, options, 8 | // ...). 9 | // 10 | // To start using the system, one needs to invoke exported `init` function from 11 | // background script (once), passing 'bg' as the name of the context, optionally 12 | // providing message handling functions. This will install onConnect listener 13 | // for incoming Port connections from all other context. 14 | // 15 | // Any other context (with arbitrary name and (optional) message handlers) also 16 | // invokes the `init` function. In this case, Port is created and connected to 17 | // background script. 18 | // 19 | // Note: due to bug https://code.google.com/p/chromium/issues/detail?id=356133 20 | // we also have dedicated name for developer tools context: 'dt'. Once this bug 21 | // is fixed, the only reserved context name will be 'bg' for background again. 22 | // 23 | // To avoid race conditions, make sure that your background script calls `init` 24 | // function after it is started, so it doesn't miss any Port connections 25 | // attempts. 26 | // 27 | // To be able to handle commands (or associated messages) in contexts (both 28 | // background and non-background), one must pass message handling functions in 29 | // `handlers` object when invoking respective `init` function for given context. 30 | // The `handlers` object is a function lookup table, i.e. object with function 31 | // names as its keys and functions (code) as corresponding values. The function 32 | // will be invoked, when given context is requested to handle message 33 | // representing command with name that can be found as a key of the `handlers` 34 | // object. Its return value (passed in callback, see below) will be treated as 35 | // value that should be passed back to the requestor. 36 | // 37 | // Each message handling function can take any number of parameters, but MUST 38 | // take callback as its last argument and invoke this callback when the message 39 | // handler is done with processing of the message (regardless if synchronous or 40 | // asynchronous). The callback takes one argument, this argument is treated as 41 | // return value of the message handler. The callback function MUST be invoked 42 | // once and only once. 43 | // 44 | // The `init` function returns (for any context it is invoked in) messaging 45 | // object with two function: `cmd` and `bcast`, both used for sending messages 46 | // to different contexts (or same context in different windows / tabs). 47 | // 48 | // Both functions behave the same way and have also the same arguments, the only 49 | // difference is that the `cmd` callback (its last argument, if provided) is 50 | // invoked with only one response value from all collected responses, while to 51 | // the `bcast` callback (if provided) we pass array with all valid responses we 52 | // collected while broadcasting given request. 53 | // 54 | // `cmd` and `bcast` functions arguments: 55 | // 56 | // (optional) [int] tabId: if not specified, broadcasted to all tabs, 57 | // if specified, sent only to given tab, can use SAME_TAB value here 58 | // (exported from this module, too) 59 | // 60 | // (optional) [array] contexts: if not specified, broadcasted to all contexts, 61 | // if specified, sent only to listed contexts (context name is provided 62 | // as the first argument when invoking the `init` function) 63 | // 64 | // (required) [string] command: name of the command to be executed 65 | // 66 | // (optional) [any type] arguments: any number of aruments that follow command 67 | // name are passed to execution handler when it is invoked 68 | // 69 | // (optional) [function(result)] callback: if provided (as last argument to 70 | // `cmd` or `bcast`), this function will be invoked when the response(s) 71 | // is/are received 72 | // 73 | // The `cmd` and `bcast` functions return `true` if the processing of the 74 | // request was successful (i.e. if all the arguments were recognized properly), 75 | // otherwise it returns `false`. 76 | // 77 | // When `cmd` or `bcast` function is invoked from background context, a set of 78 | // context instances, to which the message will be sent to, is created based on 79 | // provided arguments (tab id and context names). The set is NOT filtered by 80 | // provided command name, as background context doesn't know what message 81 | // handlers are used in all the contexts (i.e. it doesn't know the function 82 | // names in message handling lookup function tables of non-background contexts). 83 | // 84 | // When tab id or context names are NOT provided, the command is broadcasted to 85 | // all possible context instances, which the background knows about, and that 86 | // may require a lot of messaging... So for performance reasons it is wise to 87 | // provide tab-id and / or context name(s) whenever possible to reduce the size 88 | // of the context instances set as much as it gets. 89 | // 90 | // When message corresponding to command is then received in non-background 91 | // context, the handler lookup table is checked if it contains handler for 92 | // requested command name. If so, the handler is invokend and its "return value" 93 | // (passed in callback, to allow asynchronous message handling) is then sent 94 | // back to background. If there is no corresponding handler (for requested 95 | // command name), message indicating that is sent back instead. 96 | // 97 | // When background collects all the responses back from all the context 98 | // instances it sent the message to, it invokes the `cmd` or `bcast` callback, 99 | // passing the response value(s). If there was no callback provided, the 100 | // collected response values are simply dropped. 101 | // 102 | // When `cmd` or `bcast` function is invoked from non-background context, the 103 | // request message is sent to background. Background then dispatches the request 104 | // to all relevant context instances that match provided filters (again, based on 105 | // passed tab id and / or context names), and dispatches the request in favor of 106 | // the context instance that sent the original request to background. The 107 | // dispatching logic is described above (i.e. it is the same as if the request 108 | // was sent by background). 109 | // 110 | // There is one difference though: if background has corresponding handler for 111 | // requested command name (and background context is not filtered out when 112 | // creating the set of contexts), this handler is invoked (in background 113 | // context) and the "return value" is also part of the collected set of 114 | // responses. 115 | // 116 | // When all the processing in all the context instances (including background 117 | // context, if applicable) is finished and responses are collected, the 118 | // responses are sent back to the original context instance that initiated the 119 | // message processing. 120 | // 121 | // 122 | // EXAMPLE: 123 | // 124 | // background script: 125 | // ----- 126 | // 127 | // var msg = require('msg').init('bg', { 128 | // square: function(what, done) { done(what*what); } 129 | // }); 130 | // 131 | // setInterval(function() { 132 | // msg.bcast(/* ['ct'] */, 'ping', function(responses) { 133 | // console.log(responses); // ---> ['pong','pong',...] 134 | // }); 135 | // }, 1000); // broadcast 'ping' each second 136 | // 137 | // 138 | // content script: 139 | // ----- 140 | // 141 | // var msg = require('msg').init('ct', { 142 | // ping: function(done) { done('pong'); } 143 | // }); 144 | // 145 | // msg.cmd(/* ['bg'] */, 'square', 5, function(res) { 146 | // console.log(res); // ---> 25 147 | // }); 148 | // 149 | // ---------- 150 | // 151 | // For convenient sending requests from non-background contexts to 152 | // background-only (as this is most common case: non-bg context needs some info 153 | // from background), there is one more function in the messaging object returned 154 | // by the init() function. The function is called 'bg' and it prepends the list 155 | // of passed arguments with ['bg'] array, so that means the reuqest is targeted 156 | // to background-only. The 'bg' function does NOT take 'tabId' or 'contexts' 157 | // parameters, the first argument must be the command name. 158 | // 159 | // EXAMPLE: 160 | // 161 | // background script 162 | // ----- 163 | // 164 | // ( ... as above ... ) 165 | // 166 | // content script: 167 | // ----- 168 | // 169 | // var msg = require('msg').init('ct', { 170 | // ping: function(done) { done('pong'); } 171 | // }); 172 | // 173 | // msg.bg('square', 5, function(res) { 174 | // console.log(res); // ---> 25 175 | // }); 176 | // 177 | // ---------- 178 | // 179 | // There are two dedicated background handlers that, when provided in `handlers` 180 | // object for `bg` context in `init` function, are invoked by the messaging 181 | // system itself. These handlers are: 182 | // 183 | // + onConnect: function(contextName, tabId), 184 | // + onDisconnect: function(contextName, tabId) 185 | // 186 | // These two special handlers, if provided, are invoked when new Port is 187 | // connected (i.e. when `init` function is invoked in non-bg context), and 188 | // then when they are closed (disconnected) later on. This notification system 189 | // allows to maintain some state about connected contexts in extension 190 | // backround. 191 | // 192 | // Please note that unlike all other handlers passed as the `handlers` object to 193 | // `init` function, these two special handlers do NOT take callback as their 194 | // last arguments. Any return value these handlers may return is ignored. 195 | // 196 | // The `contextName` parameter is value provided to non-background `init` 197 | // function, while the `tabId` is provided by the browser. If tabId is not 198 | // provided by the browser, the `tabId` will be `Infinity`. 199 | // 200 | 201 | 202 | // constant for "same tab as me" 203 | const SAME_TAB = -1000; // was -Infinity, but JSON.stringify() + JSON.parse() don't like that value 204 | 205 | // run-time API: 206 | // variable + exported function to change it, so it can be mocked in unit tests 207 | /* global chrome */ 208 | const runtime = (typeof chrome === 'object') && chrome.runtime; 209 | // the same for devtools API: 210 | const devtools = (typeof chrome === 'object') && chrome.devtools; 211 | 212 | // utility function for looping through object's own keys 213 | // callback: function(key, value, obj) ... doesn't need to use all 3 parameters 214 | // returns object with same keys as the callback was invoked on, values are the 215 | // callback returned values ... can be of course ignored by the caller, too 216 | function forOwnProps(obj, callback) { 217 | if (typeof callback !== 'function') { 218 | return null; 219 | } 220 | const res = {}; 221 | for (const key in obj) { // eslint-disable-line no-restricted-syntax 222 | if (obj.hasOwnProperty(key)) { // eslint-disable-line no-prototype-builtins 223 | res[key] = callback(key, obj[key], obj); 224 | } 225 | } 226 | return res; 227 | } 228 | 229 | // we wrap the whole module functionality into isolated scope, so that later we 230 | // can instantiate multiple parallel scopes for unit testing. 231 | // The module will still seem to hold singleton object, because we'll create 232 | // this singleton and will export its methods as (whole) module methods. 233 | 234 | function Messaging() { 235 | // handlers available in given context (function lookup table), set in `init()` 236 | // format: 237 | // { 238 | // (string): (function), 239 | // ... 240 | // } 241 | this.handlers = {}; 242 | 243 | // id assigned by background, used in non-background contexts only 244 | // in background set to 'bg' 245 | this.id = null; 246 | 247 | // port used for communication with background (i.e. not used in background) 248 | // type: (chrome.runtime) Port 249 | this.port = null; 250 | 251 | // map of ports for connected extensions 252 | // key = extension id, value = port 253 | this.extPorts = {}; 254 | 255 | // callback lookup table: if request waits for response, this table holds 256 | // the callback function that will be invoke upon response 257 | // format: 258 | // { 259 | // (int): (function), 260 | // ... 261 | // } 262 | this.cbTable = {}; 263 | 264 | // background table of pending requests 265 | // format: 266 | // { 267 | // (string): [ { id: (int), cb: (function) }, ...], 268 | // ... 269 | // } 270 | this.pendingReqs = {}; 271 | 272 | // unique context id, used by background 273 | this.uId = 1; 274 | 275 | // request id, used by all contexts 276 | this.requestId = 1; 277 | 278 | // mapping non-background context names to objects indexed by name of the context 279 | // instances, holding { tab-id, (chrome.runtime.)Port } pairs, 280 | // used for message dispatching 281 | // format: 282 | // { 283 | // (string): { 284 | // (string): { tabId: (optional), port: }, 285 | // ... 286 | // }, 287 | // ... 288 | // } 289 | // background-only variable 290 | this.portMap = {}; 291 | 292 | // runetime and devtools references, so that we can change it in unit tests 293 | this.runtime = runtime; 294 | this.devtools = devtools; 295 | } 296 | 297 | // background function for selecting target ports to which we broadcast the request 298 | // fromBg: is the request to collect targets from bacground, or based on message? 299 | // targ*: filter for target ports 300 | // src*: information about source port 301 | // returns array of { port: (chrome.runtime.Port), id: (string) } 302 | Messaging.prototype.selectTargets = 303 | function selectTargets(fromBg, targTabId, targCategories, srcCategory, srcPortId) { 304 | const res = []; 305 | // eslint-disable-next-line no-underscore-dangle 306 | const _port = this.portMap[srcCategory] && this.portMap[srcCategory][srcPortId]; 307 | if (!fromBg && !_port) { 308 | // this should never happen, we just got request from this port! 309 | return []; 310 | } 311 | if (!fromBg && (targTabId === SAME_TAB)) { 312 | targTabId = _port.tabId; // eslint-disable-line no-param-reassign 313 | } 314 | // iterate through portMap, pick targets: 315 | forOwnProps(this.portMap, (categ, portGroup) => { 316 | if (targCategories && (targCategories.indexOf(categ) === -1)) { 317 | // we are interested only in specified contexts, 318 | // and this category is not on the list 319 | return; 320 | } 321 | forOwnProps(portGroup, (id, _ref) => { 322 | if (targTabId && (targTabId !== _ref.tabId)) { 323 | // we are interested in specified tab id, 324 | // and this id doesn't match 325 | return; 326 | } 327 | if (fromBg || (_port.port !== _ref.port)) { 328 | // do not ask me back, ask only different ports 329 | res.push({ port: _ref.port, id }); 330 | } 331 | }); 332 | }); 333 | return res; 334 | }; 335 | 336 | // message handler (useb by both background and non-backound) 337 | Messaging.prototype.onCustomMsg = function onCustomMsg(message) { 338 | /* eslint-disable no-underscore-dangle */ 339 | let _port; 340 | let _arr; 341 | let _localHandler; 342 | let _ref; 343 | let i; 344 | /* eslint-enable */ 345 | 346 | // helper functions: 347 | 348 | // send response on result (non-background): 349 | function sendResultCb(result) { 350 | if (message.sendResponse) { 351 | this.port.postMessage({ 352 | cmd: 'response', 353 | portId: this.id, 354 | reqId: message.reqId, 355 | resultValid: true, 356 | result 357 | }); 358 | } 359 | } 360 | 361 | // create callback waiting for N results, then send response (background): 362 | function createCbForMoreResults(N) { 363 | const results = []; 364 | const myId = this.runtime.id; 365 | return (result, resultValid) => { 366 | if (resultValid !== false) { // can be either `true` or `undefined` 367 | results.push(result); 368 | } 369 | N -= 1; // eslint-disable-line no-param-reassign 370 | if (!N && message.sendResponse && // eslint-disable-line no-cond-assign 371 | ( 372 | (_port = this.extPorts[message.extensionId]) || 373 | ( 374 | this.portMap[message.category] && 375 | (_port = this.portMap[message.category][message.portId]) 376 | ) 377 | ) 378 | ) { 379 | const response = { 380 | cmd: 'response', 381 | reqId: message.reqId, 382 | result: message.broadcast ? results : results[0] 383 | }; 384 | 385 | if (message.extensionId) { 386 | response.extensionId = myId; 387 | } 388 | _port.port.postMessage(response); 389 | } 390 | }; // .bind(this); 391 | } 392 | 393 | // main message processing: 394 | if (!message || !message.cmd) { 395 | return; 396 | } 397 | if (message.cmd === 'setName') { 398 | this.id = message.name; 399 | return; 400 | } 401 | if (this.id === 'bg') { 402 | // background 403 | if (message.cmd === 'request') { 404 | const targetPorts = this.selectTargets(false, message.tabId, message.contexts, 405 | message.category, message.portId); 406 | let responsesNeeded = targetPorts.length; 407 | if ((message.tabId === undefined) && 408 | (!message.contexts || (message.contexts.indexOf('bg') !== -1))) { 409 | // we are also interested in response from background itself 410 | if ( // eslint-disable-line no-cond-assign 411 | (_ref = this.handlers[message.cmdName]) && 412 | (typeof _ref === 'function') 413 | ) { 414 | _localHandler = _ref; 415 | responsesNeeded += 1; 416 | } 417 | } 418 | if (!responsesNeeded) { 419 | // no one to answer that now 420 | if ( // eslint-disable-line no-cond-assign 421 | message.sendResponse && 422 | ( 423 | (_port = this.extPorts[message.extensionId]) || 424 | ( 425 | this.portMap[message.category] && 426 | (_port = this.portMap[message.category][message.portId]) 427 | ) 428 | ) 429 | ) { 430 | const response = { 431 | cmd: 'response', 432 | reqId: message.reqId, 433 | resultValid: false, 434 | result: message.broadcast ? [] : undefined 435 | }; 436 | if (message.extensionId) { 437 | response.extensionId = this.runtime.id; 438 | } 439 | _port.port.postMessage(response); 440 | } 441 | } else { 442 | // some responses needed 443 | const cb = createCbForMoreResults.call(this, responsesNeeded); 444 | // send to target ports 445 | for (i = 0; i < targetPorts.length; i += 1) { 446 | _port = targetPorts[i]; 447 | _port.port.postMessage({ 448 | cmd: 'request', 449 | cmdName: message.cmdName, 450 | sendResponse: true, 451 | args: message.args, 452 | reqId: this.requestId 453 | }); 454 | _arr = this.pendingReqs[_port.id] || []; 455 | _arr.push({ id: this.requestId, cb }); 456 | this.pendingReqs[_port.id] = _arr; 457 | this.requestId += 1; 458 | } 459 | // get local response (if background can provide it) 460 | if (_localHandler) { 461 | message.args.push(cb); 462 | _localHandler.apply(this.handlers, message.args); 463 | } 464 | } 465 | } else if (message.cmd === 'response') { 466 | const id = message.portId || message.extensionId; 467 | _arr = this.pendingReqs[id]; // warning: IE creates a copy here! 468 | if (_arr) { 469 | // some results from given port expected, find the callback for reqId 470 | i = 0; 471 | while ((i < _arr.length) && (_arr[i].id !== message.reqId)) { i += 1; } 472 | if (i < _arr.length) { 473 | // callback found 474 | _arr[i].cb(message.result, message.resultValid); 475 | this.pendingReqs[id].splice(i, 1); // need to use orig array (IE problem) 476 | if (!this.pendingReqs[id].length) { // ... same here 477 | delete this.pendingReqs[id]; 478 | } 479 | } 480 | } 481 | } else if (message.cmd === 'updateTabId') { 482 | const context = message.context; 483 | const portId = message.portId; 484 | if ( // eslint-disable-line no-cond-assign 485 | (_port = this.portMap[context]) && 486 | (_port = _port[portId]) 487 | ) { 488 | if (typeof this.handlers.onDisconnect === 'function') { 489 | this.handlers.onDisconnect(context, _port.tabId); 490 | } 491 | _port.tabId = message.tabId; 492 | if (typeof this.handlers.onConnect === 'function') { 493 | this.handlers.onConnect(context, _port.tabId); 494 | } 495 | } 496 | } 497 | } else if (message.cmd === 'request') { // non-background 498 | _localHandler = this.handlers[message.cmdName]; 499 | if (typeof _localHandler !== 'function') { 500 | if (message.sendResponse) { 501 | this.port.postMessage({ 502 | cmd: 'response', 503 | portId: this.id, 504 | reqId: message.reqId, 505 | resultValid: false 506 | }); 507 | } 508 | } else { 509 | message.args.push(sendResultCb.bind(this)); 510 | _localHandler.apply(this.handlers, message.args); 511 | } 512 | } else if (message.cmd === 'response') { 513 | if (this.cbTable[message.reqId]) { 514 | this.cbTable[message.reqId](message.result); 515 | delete this.cbTable[message.reqId]; 516 | } 517 | } 518 | }; 519 | 520 | // invoke callbacks for pending requests and remove the requests from the structure 521 | Messaging.prototype.closePendingReqs = function closePendingReqs(portId) { 522 | let arr; 523 | if (arr = this.pendingReqs[portId]) { // eslint-disable-line no-cond-assign 524 | for (let i = 0; i < arr.length; i += 1) { 525 | arr[i].cb(undefined, false); 526 | } 527 | delete this.pendingReqs[portId]; 528 | } 529 | }; 530 | 531 | Messaging.prototype.registerExternalConnection = function regExternalConnection(extensionId, port) { 532 | this.extPorts[extensionId] = { port }; 533 | 534 | let onCustomMsg; 535 | let onDisconnect; 536 | 537 | // on disconnect: remove listeners and delete from port map 538 | function onDisconnectFn() { 539 | // listeners: 540 | port.onDisconnect.removeListener(onDisconnect); 541 | port.onMessage.removeListener(onCustomMsg); 542 | delete this.extPorts[extensionId]; 543 | // close all pending requests: 544 | this.closePendingReqs(extensionId); 545 | // invoke custom onDisconnect handler 546 | if (typeof this.handlers.onExtensionDisconnect === 'function') { 547 | this.handlers.onExtensionDisconnect(extensionId); 548 | } 549 | } 550 | 551 | // install port handlers 552 | port.onMessage.addListener(onCustomMsg = this.onCustomMsg.bind(this)); 553 | port.onDisconnect.addListener(onDisconnect = onDisconnectFn.bind(this)); 554 | // invoke custom onConnect handler 555 | if (typeof this.handlers.onExtensionConnect === 'function') { 556 | this.handlers.onExtensionConnect(extensionId); 557 | } 558 | }; 559 | 560 | Messaging.prototype.onConnectExternal = function onConnectExternal(port) { 561 | if (this.extPorts[port.sender.id]) { 562 | return; 563 | } 564 | 565 | this.registerExternalConnection(port.sender.id, port); 566 | }; 567 | 568 | // backround onConnect handler 569 | Messaging.prototype.onConnect = function onConnect(port) { 570 | // add to port map 571 | const categName = port.name || 'unknown'; 572 | const portId = `${categName}-${this.uId}`; 573 | this.uId += 1; 574 | let portCateg = this.portMap[categName] || {}; 575 | let tabId = (port.sender && port.sender.tab && port.sender.tab.id) || Infinity; 576 | portCateg[portId] = { port, tabId }; 577 | this.portMap[categName] = portCateg; 578 | let onCustomMsg; 579 | let onDisconnect; 580 | // on disconnect: remove listeners and delete from port map 581 | function onDisconnectFn() { 582 | // listeners: 583 | port.onDisconnect.removeListener(onDisconnect); 584 | port.onMessage.removeListener(onCustomMsg); 585 | // port map: 586 | portCateg = this.portMap[categName]; 587 | let _port; // eslint-disable-line no-underscore-dangle 588 | if (portCateg && (_port = portCateg[portId])) { // eslint-disable-line no-cond-assign 589 | tabId = _port.tabId; 590 | delete portCateg[portId]; 591 | } 592 | // close all pending requests: 593 | this.closePendingReqs(portId); 594 | // invoke custom onDisconnect handler 595 | if (typeof this.handlers.onDisconnect === 'function') { 596 | this.handlers.onDisconnect(categName, tabId); 597 | } 598 | } 599 | // install port handlers 600 | port.onMessage.addListener(onCustomMsg = this.onCustomMsg.bind(this)); 601 | port.onDisconnect.addListener(onDisconnect = onDisconnectFn.bind(this)); 602 | // ask counter part to set its id 603 | port.postMessage({ cmd: 'setName', name: portId }); 604 | // invoke custom onConnect handler 605 | if (typeof this.handlers.onConnect === 'function') { 606 | this.handlers.onConnect(categName, tabId); 607 | } 608 | }; 609 | 610 | // create main messaging object, hiding all the complexity from the user 611 | // it takes name of local context `myContextName` 612 | // 613 | // the returned object has two main functions: cmd and bcast 614 | // 615 | // they behave the same way and have also the same arguments, the only 616 | // difference is that to `cmd` callback (if provided) is invoked with only one 617 | // response value from all possible responses, while to `bcast` callback (if 618 | // provided) we pass array with all valid responses we collected while 619 | // broadcasting given request. 620 | // 621 | // functions arguments: 622 | // 623 | // (optional) [int] tabId: if not specified, broadcasted to all tabs, 624 | // if specified, sent only to given tab, can use SAME_TAB value here 625 | // 626 | // (optional) [array] contexts: if not specified, broadcasted to all contexts, 627 | // if specified, sent only to listed contexts 628 | // 629 | // (required) [string] command: name of the command to be executed 630 | // 631 | // (optional) [any type] arguments: any number of aruments that follow command 632 | // name are passed to execution handler when it is invoked 633 | // 634 | // (optional) [function(result)] callback: if provided (as last argument to 635 | // `cmd` or `bcast`) this function will be invoked when the response(s) 636 | // is/are received 637 | // 638 | // the functions return `true` if the processing of the request was successful 639 | // (i.e. if all the arguments were recognized properly), otherwise it returns 640 | // `false`. 641 | // 642 | // for non-bg contexts there is one more function in the messaging object 643 | // available: 'bg' function, that is the same as 'cmd', but prepends the list of 644 | // arguments with ['bg'], so that the user doesn't have to write it when 645 | // requesting some info in non-bg context from background. 646 | // 647 | Messaging.prototype.createMsgObject = function createMsgObject(myContextName) { 648 | // generator for functions `cmd` and `bcast` 649 | function createFn(broadcast) { 650 | // helper function for invoking provided callback in background 651 | function createCbForMoreResults(N, callback) { 652 | const results = []; 653 | return (result, resultValid) => { 654 | if (resultValid) { 655 | results.push(result); 656 | } 657 | N -= 1; // eslint-disable-line no-param-reassign 658 | if ((N <= 0) && callback) { 659 | callback(broadcast ? results : results[0]); 660 | } 661 | }; 662 | } 663 | // generated function: 664 | return function _msg() { 665 | // process arguments: 666 | if (!arguments.length) { 667 | // at least command name must be provided 668 | return false; 669 | } 670 | if (!this.id) { 671 | // since we learn our id of non-background context in asynchronous 672 | // message, we may need to wait for it... 673 | const _ctx = this; 674 | const _args = arguments; 675 | setTimeout(() => { _msg.apply(_ctx, _args); }, 1); 676 | return true; 677 | } 678 | let tabId; 679 | let contexts; 680 | let cmdName; 681 | const args = []; 682 | let callback; 683 | let curArg = 0; 684 | let argsLimit = arguments.length; 685 | // check if we have callback: 686 | if (typeof arguments[argsLimit - 1] === 'function') { 687 | argsLimit -= 1; 688 | callback = arguments[argsLimit]; 689 | } 690 | // other arguments: 691 | while (curArg < argsLimit) { 692 | const arg = arguments[curArg]; 693 | curArg += 1; 694 | if (cmdName !== undefined) { 695 | args.push(arg); 696 | } else { 697 | // we don't have command name yet... 698 | switch (typeof (arg)) { 699 | // tab id 700 | case 'number': 701 | if (tabId !== undefined) { 702 | return false; // we already have tab id --> invalid args 703 | } 704 | tabId = arg; 705 | break; 706 | // contexts (array) 707 | case 'object': 708 | if ((typeof (arg.length) === 'undefined') || (contexts !== undefined)) { 709 | return false; // we either have it, or it is not array-like object 710 | } 711 | contexts = arg; 712 | break; 713 | // command name 714 | case 'string': 715 | cmdName = arg; 716 | break; 717 | // anything else --> error 718 | default: 719 | return false; 720 | } 721 | } 722 | } 723 | if (cmdName === undefined) { 724 | return false; // command name is mandatory 725 | } 726 | // store the callback and issue the request (message) 727 | if ('bg' === this.id) { 728 | const targetPorts = this.selectTargets(true, tabId, contexts); 729 | const responsesNeeded = targetPorts.length; 730 | const cb = createCbForMoreResults.call(this, responsesNeeded, callback); 731 | // send to target ports 732 | for (let i = 0; i < targetPorts.length; i += 1) { 733 | const _port = targetPorts[i]; 734 | _port.port.postMessage({ 735 | cmd: 'request', 736 | cmdName, 737 | sendResponse: true, 738 | args, 739 | reqId: this.requestId 740 | }); 741 | const _arr = this.pendingReqs[_port.id] || []; 742 | _arr.push({ id: this.requestId, cb }); 743 | this.pendingReqs[_port.id] = _arr; 744 | this.requestId += 1; 745 | } 746 | if (!targetPorts.length) { 747 | // no one to respond, invoke the callback (if provided) right away 748 | cb(null, false); 749 | } 750 | } else { 751 | if (callback) { 752 | this.cbTable[this.requestId] = callback; 753 | } 754 | this.port.postMessage({ 755 | cmd: 'request', 756 | cmdName, 757 | reqId: this.requestId, 758 | sendResponse: (callback !== undefined), 759 | broadcast, 760 | category: myContextName, 761 | portId: this.id, 762 | tabId, 763 | contexts, 764 | args 765 | }); 766 | this.requestId += 1; 767 | } 768 | // everything went OK 769 | return true; 770 | }.bind(this); 771 | } 772 | 773 | function createCmdExtFn() { 774 | return function _msg(extensionId, commandName) { 775 | // process arguments: 776 | if (arguments.length < 2) { 777 | // at least extension id and command name must be provided 778 | return false; 779 | } 780 | 781 | if (this.id !== 'bg') { 782 | return false; // only background can send messagess to another extensions 783 | } 784 | 785 | const args = Array.prototype.slice.call(arguments, 2); 786 | let callback; 787 | if (typeof (args[args.length - 1]) === 'function') { 788 | callback = args.pop(); 789 | } 790 | 791 | const _port = this.extPorts[extensionId]; 792 | if (!_port) { 793 | // no one to respond, invoke the callback (if provided) right away 794 | if (callback) { callback(); } 795 | 796 | return true; 797 | } 798 | 799 | _port.port.postMessage({ 800 | cmd: 'request', 801 | cmdName: commandName, 802 | sendResponse: true, 803 | args, 804 | reqId: this.requestId, 805 | extensionId: this.runtime.id 806 | }); 807 | 808 | const _arr = this.pendingReqs[extensionId] || []; 809 | _arr.push({ id: this.requestId, 810 | cb(result/* , resultValid/**/) { // ignore 'resultValid' because it is not applicable here 811 | if (callback) { callback(result); } 812 | } 813 | }); 814 | this.pendingReqs[extensionId] = _arr; 815 | this.requestId += 1; 816 | 817 | // everything went OK 818 | return true; 819 | }.bind(this); 820 | } 821 | 822 | // returned object: 823 | const res = { 824 | cmd: createFn.call(this, false), 825 | bcast: createFn.call(this, true) 826 | }; 827 | 828 | // for more convenience (when sending request from non-bg to background only) 829 | // adding 'bg(, ...)' function, that is equivalent to "cmd(['bg'], , ...)" 830 | if (myContextName !== 'bg') { 831 | res.bg = function bg() { 832 | if (0 === arguments.length || 'string' !== typeof (arguments[0])) { 833 | return false; 834 | } 835 | const args = [['bg']]; 836 | for (let i = 0; i < arguments.length; i += 1) { args.push(arguments[i]); } 837 | return res.cmd(...args); 838 | }; 839 | } else { 840 | res.connectExt = function connectExt(id) { 841 | if (this.extPorts[id]) { // already connected 842 | return true; 843 | } 844 | const port = this.runtime.connect(id); 845 | this.registerExternalConnection(id, port); 846 | return undefined; 847 | }.bind(this); 848 | res.cmdExt = createCmdExtFn.call(this); 849 | } 850 | 851 | return res; 852 | }; 853 | 854 | // init function, exported 855 | // 856 | // takes mandatory `context`, it is any string (e.g. 'ct', 'popup', ...), 857 | // only one value is of special meaning: 'bg' ... must be used for initializing 858 | // of the background part, any other context is considered non-background 859 | // 860 | // optionally takes `handlers`, which is object mapping function names to 861 | // function codes, that is used as function lookup table. each message handling 862 | // function MUST take callback as its last argument and invoke this callback 863 | // when the message handler is done with processing of the message (regardless 864 | // if synchronous or asynchronous). the callback takes one argument, this 865 | // argument is treated as return value of the message handler. 866 | // 867 | // for background (`context` is 'bg'): installs onConnect listener 868 | // for non-background context it connects to background 869 | // 870 | Messaging.prototype.init = function init(context, handlers) { 871 | // set message handlers (optional) 872 | this.handlers = handlers || {}; 873 | 874 | // listener references 875 | let onDisconnect; 876 | let onCustomMsg; 877 | 878 | // helper function: 879 | function onDisconnectFn() { 880 | this.port.onDisconnect.removeListener(onDisconnect); 881 | this.port.onMessage.removeListener(onCustomMsg); 882 | } 883 | 884 | let tabId; 885 | function updateTabId() { 886 | if (!this.id) { 887 | setTimeout(updateTabId.bind(this), 1); 888 | return; 889 | } 890 | this.port.postMessage({ 891 | cmd: 'updateTabId', 892 | context, 893 | portId: this.id, 894 | tabId 895 | }); 896 | } 897 | 898 | if (context === 'bg') { 899 | // background 900 | this.id = 'bg'; 901 | this.runtime.onConnect.addListener(this.onConnect.bind(this)); 902 | this.runtime.onConnectExternal.addListener(this.onConnectExternal.bind(this)); 903 | } else { 904 | // anything else than background 905 | this.port = this.runtime.connect({ name: context }); 906 | this.port.onMessage.addListener(onCustomMsg = this.onCustomMsg.bind(this)); 907 | this.port.onDisconnect.addListener(onDisconnect = onDisconnectFn.bind(this)); 908 | // tabId update for developer tools 909 | // unfortunately we need dedicated name for developer tools context, due to 910 | // this bug: https://code.google.com/p/chromium/issues/detail?id=356133 911 | // ... we are not able to tell if we are in DT context otherwise :( 912 | if ( // eslint-disable-line no-cond-assign 913 | (context === 'dt') && this.devtools && 914 | (tabId = this.devtools.inspectedWindow) && 915 | (typeof (tabId = tabId.tabId) === 'number') 916 | ) { 917 | updateTabId.call(this); 918 | } 919 | } 920 | 921 | return this.createMsgObject(context); 922 | }; 923 | 924 | 925 | // singleton representing this module 926 | const singleton = new Messaging(); 927 | 928 | // helper function to install methods used for unit tests 929 | function installUnitTestMethods(target, delegate) { 930 | /* eslint-disable no-underscore-dangle, no-param-reassign */ 931 | // setters 932 | target.__setRuntime = (rt) => { delegate.runtime = rt; return target; }; 933 | target.__setDevTools = (dt) => { delegate.devtools = dt; return target; }; 934 | // getters 935 | target.__getId = () => delegate.id; 936 | target.__getPort = () => delegate.port; 937 | target.__getPortMap = () => delegate.portMap; 938 | target.__getHandlers = () => delegate.handlers; 939 | target.__getPendingReqs = () => delegate.pendingReqs; 940 | /* eslint-enable */ 941 | } 942 | 943 | export default { 944 | // same tab id 945 | SAME_TAB, 946 | // see description for init function above 947 | init: singleton.init.bind(singleton), 948 | // --- for unit tests --- 949 | // allow unit testing of the main module: 950 | __allowUnitTests() { installUnitTestMethods(this, singleton); }, 951 | // context cloning 952 | __createClone() { 953 | const clone = new Messaging(); 954 | clone.SAME_TAB = SAME_TAB; 955 | installUnitTestMethods(clone, clone); 956 | return clone; 957 | } 958 | }; 959 | -------------------------------------------------------------------------------- /src/js/modules/msg.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | // mocked chrome.runtime 4 | import ChromeRuntime from './chrome.runtime.mock'; 5 | 6 | // mocked chrome.devtools 7 | import { devtools } from './chrome.devtools.mock'; 8 | import bgMain from './msg'; // for background, we can load the main module 9 | 10 | const runtime1 = new ChromeRuntime(); 11 | const runtime2 = new ChromeRuntime(); 12 | 13 | // tested messaging module for background context and other contexts 14 | const ctxMain = []; // these will stay for inspecting internals, while 15 | let bg; 16 | const ctx = []; // these will become messaging objects later on 17 | let bg1; // messaging object for cross-extension messaging 18 | // number of non-background contexts used in the test 19 | const CTX_COUNT = 20; 20 | // background handlers, for testing custom onConnect and onDisconnect callbacks 21 | let bgHandlers; 22 | let bgExtHandlers; 23 | 24 | // load isolated module copies, so that we can simulate 25 | // multiple parallel contexts 26 | 27 | // install unit test inspecting methods 28 | bgMain.__allowUnitTests(); // eslint-disable-line no-underscore-dangle 29 | const bgExt = bgMain.__createClone(); // eslint-disable-line no-underscore-dangle 30 | 31 | // non-bg copies: 32 | for (let i = 0; i < CTX_COUNT; i += 1) { 33 | ctxMain.push(bgMain.__createClone().__setRuntime(runtime1)); 34 | } 35 | 36 | // stores history of handler invocations (in all contexts) 37 | let handlerLog = []; 38 | // function dumpLog() { console.log(JSON.stringify(handlerLog, null, 4)); } 39 | // helper to add items to log 40 | function addLog(context, idx, cmd, args, retVal) { 41 | handlerLog.push({ context, idx, cmd, args, retVal }); 42 | } 43 | // factory for generating context handlers 44 | function createHandlers(context, idx) { 45 | // shared handlers: 46 | const res = { 47 | // SYNCHRONOUS 48 | // log passed arguments to handlerLog 49 | log: (...args) => { 50 | const done = args[args.length - 1]; // last arg is always callback 51 | addLog(context, idx, 'log', args.slice(0, args.length - 1)); 52 | done(); 53 | }, 54 | // ASYNCHRONOUS 55 | // generates random number 0..max-1 56 | random(max, done) { 57 | const rand = Math.floor(Math.random() * max); 58 | addLog(context, idx, 'random', [max], rand); 59 | setImmediate(() => { 60 | done(rand); 61 | }); 62 | }, 63 | // BLOCKING FOREVER 64 | // invalid handler (doesn't invoke done()), used for testing tab-closing 65 | block: () => { 66 | addLog(context, idx, 'block', []); 67 | } 68 | }; 69 | const ctxTypeCmd = `${context}_cmd`; 70 | const ctxInstCmd = `${context}_${idx}_cmd`; 71 | // CONTEXT-TYPE-ONLY handler, echoes passed argument 72 | res[ctxTypeCmd] = (what, done) => { 73 | addLog(context, idx, ctxTypeCmd, [what], what); 74 | done(what); 75 | }; 76 | // CONTEXT-INSTANCE-ONLY handler, returns 'hello world' 77 | res[ctxInstCmd] = (done) => { 78 | addLog(context, idx, ctxInstCmd, [], 'hello world'); 79 | done('hello world'); 80 | }; 81 | // FOR 'bg' CONTEXT, prepare (but do not install under correct name) custom 82 | // onConnect and onDisconnect handlers 83 | if (context === 'bg') { 84 | res._onConnect = (ctxName, tabId) => { 85 | addLog(context, idx, 'onConnect', [ctxName, tabId]); 86 | }; 87 | res._onDisconnect = (ctxName, tabId) => { 88 | addLog(context, idx, 'onDisconnect', [ctxName, tabId]); 89 | }; 90 | res._onExtensionConnect = (extensionId) => { 91 | addLog(context, idx, 'onExtensionConnect', [extensionId]); 92 | }; 93 | res._onExtensionDisconnect = (extensionId) => { 94 | addLog(context, idx, 'onExtensionDisconnect', [extensionId]); 95 | }; 96 | } 97 | // 98 | return res; 99 | } 100 | 101 | // non-background contexts definitions: // generated tabId: 102 | const ctxDefs = [ 103 | { name: 'ct', idx: 1 }, // gener. tabId: 1 104 | { name: 'dt', idx: 1 }, // gener. tabId: 1 105 | { name: 'ct', idx: 2 }, // gener. tabId: 2 106 | { name: 'dt', idx: 2 }, // gener. tabId: 2 107 | { name: 'popup', idx: 9 }, // gener. tabId: 3 108 | { name: 'options', idx: 9 }, // gener. tabId: 3 109 | { name: 'ct', idx: 3 }, // gener. tabId: 4 110 | { name: 'dt', idx: 3 }, // gener. tabId: 4 111 | { name: 'ct', idx: 4 }, // gener. tabId: 5 112 | { name: 'dt', idx: 4 }, // gener. tabId: 5 113 | { name: 'ct', idx: 5 }, // gener. tabId: 6 114 | { name: 'dt', idx: 5 }, // gener. tabId: 6 115 | { name: 'popup', idx: 10 }, // gener. tabId: 7 116 | { name: 'options', idx: 10 }, // gener. tabId: 7 117 | { name: 'ct', idx: 6 }, // gener. tabId: 8 118 | { name: 'dt', idx: 6 }, // gener. tabId: 8 119 | { name: 'ct', idx: 7 }, // gener. tabId: 9 120 | { name: 'dt', idx: 7 }, // gener. tabId: 9 121 | { name: 'ct', idx: 8 }, // gener. tabId: 10 122 | { name: 'dt', idx: 8 }, // gener. tabId: 10 123 | ]; 124 | 125 | // 126 | // MAIN 127 | // 128 | describe('messaging module', () => { 129 | beforeEach(() => { handlerLog = []; }); 130 | 131 | it('should export init() function', () => { 132 | assert.strictEqual(bgMain && typeof bgMain.init, 'function'); 133 | }); 134 | 135 | it('should init() and return msg object with cmd(), bcast() and bg()', (done) => { 136 | let i; 137 | runtime1.__resetTabId(); 138 | runtime2.__resetTabId(); 139 | bgMain.__setRuntime(runtime1); 140 | bgExt.__setRuntime(runtime2); 141 | let pm = bgMain.__getPortMap(); 142 | assert.deepEqual({}, pm); 143 | pm = bgExt.__getPortMap(); 144 | assert.deepEqual({}, pm); 145 | // background 146 | bg = bgMain.init('bg', bgHandlers = createHandlers('bg', 1)); 147 | assert(typeof bg === 'object'); 148 | assert(typeof bg.cmd === 'function'); 149 | assert(typeof bg.bcast === 'function'); 150 | assert(typeof bg.connectExt === 'function'); 151 | assert(typeof bg.cmdExt === 'function'); 152 | assert(typeof bg.bg === 'undefined'); 153 | // background for cross-extension messaging 154 | bg1 = bgExt.init('bg', bgExtHandlers = createHandlers('bg', 2)); 155 | assert(typeof bg1 === 'object'); 156 | assert(typeof bg1.cmd === 'function'); 157 | assert(typeof bg1.bcast === 'function'); 158 | assert(typeof bg1.connectExt === 'function'); 159 | assert(typeof bg1.cmdExt === 'function'); 160 | assert(typeof bg1.bg === 'undefined'); 161 | 162 | // first 6 context only! 163 | for (i = 0; i < 6; i += 1) { 164 | const def = ctxDefs[i]; 165 | if (def.name === 'dt') { 166 | devtools.__setTabId((i + 1) / 2); 167 | ctxMain[i].__setDevTools(devtools); 168 | } 169 | ctx.push(ctxMain[i].init(def.name, createHandlers(def.name, def.idx))); 170 | } 171 | // testing the first one only, the remaining ones should be the same 172 | assert(typeof ctx[0] === 'object'); 173 | assert(typeof ctx[0].cmd === 'function'); 174 | assert(typeof ctx[0].bcast === 'function'); 175 | assert(typeof ctx[0].bg === 'function'); 176 | // make sure we don't invoke onConnect background handler (it is not installed yet) 177 | let counter = 3; 178 | setImmediate(function _f() { 179 | assert(handlerLog.length === 0); // no onConnect invoked 180 | if (counter -= 1) { // eslint-disable-line no-cond-assign 181 | setTimeout(_f, 0); // let dt's update their ids 182 | } else { 183 | done(); 184 | } 185 | }); 186 | }); 187 | 188 | it('should set ids correctly', () => { 189 | assert(bgMain.__getId() === 'bg'); 190 | assert(ctxMain[0].__getId() === 'ct-1'); 191 | }); 192 | 193 | it('should invoke onConnect background handler for newly connected ports, ' + 194 | 'devTools should be updating their tabIds correctly', (done) => { 195 | // install onConnect / onDisconnect handlers 196 | bgHandlers.onConnect = bgHandlers._onConnect; 197 | bgHandlers.onDisconnect = bgHandlers._onDisconnect; 198 | for (let i = 6; i < CTX_COUNT; i += 1) { 199 | const def = ctxDefs[i]; 200 | if (def.name === 'dt') { 201 | devtools.__setTabId((i + 1) / 2); 202 | ctxMain[i].__setDevTools(devtools); 203 | } 204 | ctx.push(ctxMain[i].init(def.name, createHandlers(def.name, def.idx))); 205 | } 206 | // custom onConnect handlers invocations 207 | setImmediate(function _f() { 208 | // (20 - 6) connect()s + (2 * 6) disconnect/connect updates for 'dt's 209 | if (handlerLog.length !== 26) { setImmediate(_f); return; } 210 | let log; 211 | const stat = { ct: [], dt: [], popup: [], options: [] }; 212 | for (let i = 0; i < 14; i += 1) { 213 | log = handlerLog[i]; 214 | assert(log.context === 'bg'); 215 | assert(log.cmd === 'onConnect'); 216 | log = log.args; 217 | stat[log[0]].push(log[1]); 218 | } 219 | for (let i = 14; i < 26; i += 1) { 220 | log = handlerLog[i]; 221 | assert(log.context === 'bg'); 222 | // tab id updates for 'dt' 223 | assert(((i % 2) ? 'onConnect' : 'onDisconnect') === log.cmd); 224 | log = log.args; 225 | stat[log[0]].push(log[1]); 226 | } 227 | let arr = [4, 5, 6, 8, 9, 10]; // tab ids for ct contexts 228 | assert(arr.length === stat.ct.length); 229 | for (let i = 0; i < arr.length; i += 1) { assert(stat.ct[i] === arr[i]); } 230 | arr = [Infinity, Infinity, Infinity, Infinity, Infinity, Infinity, Infinity, 231 | 4, Infinity, 5, Infinity, 6, Infinity, 8, Infinity, 9, Infinity, 10]; 232 | assert(arr.length === stat.dt.length); 233 | arr.sort((a, b) => a - b); 234 | stat.dt.sort((a, b) => a - b); 235 | for (let i = 0; i < arr.length; i += 1) { assert(stat.dt[i] === arr[i]); } 236 | assert(stat.popup.length === 1); 237 | assert(stat.popup[0] === 7); 238 | assert(stat.options.length === 1); 239 | assert(stat.options[0] === 7); 240 | done(); 241 | }); 242 | }); 243 | 244 | it('should set portMap in bg context correctly', () => { 245 | const pm = bgMain.__getPortMap(); 246 | assert(Object.keys(pm).length === 4); // ct, dt, popup, options 247 | assert(pm.ct); 248 | assert(Object.keys(pm.ct).length === 8); // 8 x 'ct' context 249 | assert(pm.dt); 250 | assert(Object.keys(pm.dt).length === 8); // 8 x 'dt' context 251 | assert(pm.popup); 252 | assert(Object.keys(pm.popup).length === 2); // 2 x 'popup' context 253 | assert(pm.options); 254 | assert(Object.keys(pm.options).length === 2); // 2 x 'options' context 255 | }); 256 | 257 | it('should not set portMap in non-bg context', () => { 258 | assert.deepEqual({}, ctxMain[0].__getPortMap()); 259 | }); 260 | 261 | it('should set local callback tables (msg handlers)', () => { 262 | let handlers = bgMain.__getHandlers(); 263 | assert(typeof handlers === 'object'); 264 | // log, random, invalid, bg_cmd, bg_1_cmd, _onConnect, _onDisconnect, 265 | // _onExtensionConnect, _onExtensionDisconnect, onConnect, onDisconnect 266 | assert(Object.keys(handlers).length === 11); 267 | handlers = ctxMain[0].__getHandlers(); 268 | assert(typeof handlers === 'object'); 269 | assert(Object.keys(handlers).length === 5); // log, random, invalid, ct_cmd, ct_1_cmd 270 | }); 271 | 272 | it('should return false when invalid arguments are passed to cmd(), bcast() and bg()', () => { 273 | // cmd 274 | assert(ctx[0].cmd() === false); 275 | assert(ctx[0].cmd(1) === false); 276 | assert(ctx[0].cmd(['bg']) === false); 277 | assert(ctx[0].cmd(1, ['ct']) === false); 278 | assert(ctx[0].cmd(['ct'], 1) === false); 279 | assert(ctx[0].cmd(['ct'], ['dt'], 'log') === false); 280 | assert(ctx[0].cmd(1, 2, 'log') === false); 281 | // bcast 282 | assert(ctx[0].bcast() === false); 283 | assert(ctx[0].bcast(1) === false); 284 | assert(ctx[0].bcast(['bg']) === false); 285 | assert(ctx[0].bcast(1, ['ct']) === false); 286 | assert(ctx[0].bcast(['ct'], 1) === false); 287 | assert(ctx[0].bcast(['ct'], ['dt'], 'log') === false); 288 | assert(ctx[0].bcast(1, 2, 'log') === false); 289 | // bg 290 | assert(ctx[0].bg() === false); 291 | assert(ctx[0].bg(1) === false); 292 | assert(ctx[0].bg(['bg']) === false); 293 | assert(ctx[0].bg(1, ['ct']) === false); 294 | assert(ctx[0].bg(['ct'], 1) === false); 295 | assert(ctx[0].bg(['ct'], ['dt'], 'log') === false); 296 | assert(ctx[0].bg(1, 2, 'log') === false); 297 | assert(ctx[0].bg(['bg'], 'log') === false); 298 | assert(ctx[0].bg(1, 'log') === false); 299 | }); 300 | 301 | it('should pass 0 args from ctx to bg', (done) => { 302 | const res = ctx[0].bg('log'); 303 | assert(res === true); 304 | setImmediate(function _f() { 305 | if (handlerLog.length !== 1) { setImmediate(_f); return; } 306 | const log = handlerLog[0]; 307 | assert(log.context === 'bg'); 308 | assert(log.cmd === 'log'); 309 | assert(handlerLog[0].args.length === 0); 310 | done(); 311 | }); 312 | }); 313 | 314 | it('should pass multiple args from ctx to bg', (done) => { 315 | const res = ctx[0].bg('log', true, 0.1, 'str', ['a', 'b'], { o: 1, p: 2 }, null, undefined, 1); 316 | assert(res === true); 317 | setImmediate(function _f() { 318 | if (handlerLog.length !== 1) { setImmediate(_f); return; } 319 | const log = handlerLog[0]; 320 | assert(log.context === 'bg'); 321 | assert(log.cmd === 'log'); 322 | const args = log.args; 323 | assert(args.length === 8); 324 | assert(args[0] === true); 325 | assert(args[1] === 0.1); 326 | assert(args[2] === 'str'); 327 | assert.deepEqual(['a', 'b'], args[3]); 328 | assert.deepEqual({ o: 1, p: 2 }, args[4]); 329 | assert(args[5] === null); 330 | assert((args[6] === undefined) || (args[6] === null)); 331 | assert(args[7] === 1); 332 | done(); 333 | }); 334 | }); 335 | 336 | it('should invoke provided callback (0 args, ctx to bg, no return val)', (done) => { 337 | const res = ctx[0].bg('log', (result) => { addLog(0, 0, 'callback', result); }); 338 | assert(true === res); 339 | setImmediate(function _f() { 340 | if (2 !== handlerLog.length) { setImmediate(_f); return; } 341 | let log = handlerLog[0]; 342 | assert('bg' === log.context); 343 | assert('log' === log.cmd); 344 | assert(0 === log.args.length); 345 | log = handlerLog[1]; 346 | assert('callback' === log.cmd); 347 | assert(undefined === log.args); 348 | done(); 349 | }); 350 | }); 351 | 352 | it('should invoke provided callback (2 args, ctx to bg, no return val)', (done) => { 353 | const res = ctx[0].bg('log', 1, 2, (result) => { addLog(0, 0, 'callback', result); }); 354 | assert(true === res); 355 | setImmediate(function _f() { 356 | if (2 !== handlerLog.length) { setImmediate(_f); return; } 357 | let log = handlerLog[0]; 358 | assert('bg' === log.context); 359 | assert('log' === log.cmd); 360 | assert(2 === log.args.length); 361 | log = handlerLog[1]; 362 | assert('callback' === log.cmd); 363 | assert(undefined === log.args); 364 | done(); 365 | }); 366 | }); 367 | 368 | it('should invoke provided callback (ctx to bg, return val)', (done) => { 369 | const res = ctx[0].bg('random', 10, (result) => { addLog(0, 0, 'callback', result); }); 370 | assert(true === res); 371 | setImmediate(function _f() { 372 | if (2 !== handlerLog.length) { setImmediate(_f); return; } 373 | let log = handlerLog[0]; 374 | assert('bg' === log.context); 375 | assert('random' === log.cmd); 376 | assert(1 === log.args.length); 377 | assert(10 === log.args[0]); 378 | let _res; 379 | assert(typeof (_res = log.retVal) === 'number'); 380 | log = handlerLog[1]; 381 | assert('callback' === log.cmd); 382 | assert('number' === typeof log.args); 383 | assert(_res === log.args); 384 | done(); 385 | }); 386 | }); 387 | 388 | it('should pass 0 args from bg to (single) ctx', (done) => { 389 | const res = bg.cmd(4, ['ct'], 'log'); 390 | assert(true === res); 391 | setImmediate(function _f() { 392 | if (1 !== handlerLog.length) { setImmediate(_f); return; } 393 | const log = handlerLog[0]; 394 | assert('ct' === log.context); 395 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 396 | assert('log' === log.cmd); 397 | assert(0 === handlerLog[0].args.length); 398 | done(); 399 | }); 400 | }); 401 | 402 | it('should pass multiple args from bg to (single) ctx', (done) => { 403 | const res = bg.cmd(4, ['ct'], 'log', true, 0.1, 'str', ['a', 'b'], { o: 1, p: 2 }, null, undefined, 1); 404 | assert(true === res); 405 | setImmediate(function _f() { 406 | if (1 !== handlerLog.length) { setImmediate(_f); return; } 407 | const log = handlerLog[0]; 408 | assert('ct' === log.context); 409 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 410 | assert('log' === log.cmd); 411 | const args = log.args; 412 | assert(8 === args.length); 413 | assert(true === args[0]); 414 | assert(0.1 === args[1]); 415 | assert('str' === args[2]); 416 | assert.deepEqual(['a', 'b'], args[3]); 417 | assert.deepEqual({ o: 1, p: 2 }, args[4]); 418 | assert(null === args[5]); 419 | assert((undefined === args[6]) || (null === args[6])); 420 | assert(1 === args[7]); 421 | done(); 422 | }); 423 | }); 424 | 425 | it('should invoke provided callback (0 args, bg to (single) ctx, no return val)', (done) => { 426 | const res = bg.cmd(4, ['ct'], 'log', (result) => { addLog(0, 0, 'callback', result); }); 427 | assert(true === res); 428 | setImmediate(function _f() { 429 | if (2 !== handlerLog.length) { setImmediate(_f); return; } 430 | let log = handlerLog[0]; 431 | assert('ct' === log.context); 432 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 433 | assert('log' === log.cmd); 434 | assert(0 === log.args.length); 435 | log = handlerLog[1]; 436 | assert('callback' === log.cmd); 437 | assert(undefined === log.args); 438 | done(); 439 | }); 440 | }); 441 | 442 | it('should invoke provided callback (2 args, bg to (single) ctx, no return val)', (done) => { 443 | const res = bg.cmd(4, ['ct'], 'log', 1, 2, (result) => { addLog(0, 0, 'callback', result); }); 444 | assert(true === res); 445 | setImmediate(function _f() { 446 | if (2 !== handlerLog.length) { setImmediate(_f); return; } 447 | let log = handlerLog[0]; 448 | assert('ct' === log.context); 449 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 450 | assert('log' === log.cmd); 451 | assert(2 === log.args.length); 452 | log = handlerLog[1]; 453 | assert('callback' === log.cmd); 454 | assert(undefined === log.args); 455 | done(); 456 | }); 457 | }); 458 | 459 | it('should invoke provided callback (bg to (single) ctx, return val)', (done) => { 460 | const res = bg.cmd(4, ['ct'], 'random', 10, (result) => { addLog(0, 0, 'callback', result); }); 461 | assert(true === res); 462 | setImmediate(function _f() { 463 | if (2 !== handlerLog.length) { setImmediate(_f); return; } 464 | let log = handlerLog[0]; 465 | assert('ct' === log.context); 466 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 467 | assert('random' === log.cmd); 468 | assert(1 === log.args.length); 469 | assert(10 === log.args[0]); 470 | let _res; 471 | assert(typeof (_res = log.retVal) === 'number'); 472 | log = handlerLog[1]; 473 | assert('callback' === log.cmd); 474 | assert(typeof (log.args) === 'number'); 475 | assert(_res === log.args); 476 | done(); 477 | }); 478 | }); 479 | 480 | it('should match multiple requests with corresponding responses (ctx to bg)', (done) => { 481 | const res1 = ctx[0].bg('random', 100, (result) => { addLog(0, 0, 'cb1', result); }); 482 | const res2 = ctx[0].bg('log', 1, 2, (result) => { addLog(0, 0, 'cb2', result); }); 483 | assert(true === res1); 484 | assert(true === res2); 485 | setImmediate(function _f() { 486 | if (4 !== handlerLog.length) { setImmediate(_f); return; } 487 | let log = handlerLog[0]; 488 | assert('bg' === log.context); 489 | assert('random' === log.cmd); 490 | const _res = log.retVal; 491 | log = handlerLog[1]; 492 | assert('bg' === log.context); 493 | assert('log' === log.cmd); 494 | // interesting part here: singe random() is async, its callback (cb1) will 495 | // be invoked after callback of (sync) log(), i.e. cb2... 496 | // this way we can verify the request-responses are matched accordingly, 497 | // as first request should be matched with second response, and second 498 | // request with first response... 499 | log = handlerLog[2]; 500 | assert('cb2' === log.cmd); 501 | assert(undefined === log.result); 502 | log = handlerLog[3]; 503 | assert('cb1' === log.cmd); 504 | assert(_res === log.args); 505 | done(); 506 | }); 507 | }); 508 | 509 | it('should match multiple requests with corresponding responses (bg to (single) ctx)', (done) => { 510 | const res1 = bg.cmd(4, ['ct'], 'random', 100, (result) => { addLog(0, 0, 'cb1', result); }); 511 | const res2 = bg.cmd(4, ['ct'], 'log', 1, 2, (result) => { addLog(0, 0, 'cb2', result); }); 512 | assert(true === res1); 513 | assert(true === res2); 514 | setImmediate(function _f() { 515 | if (4 !== handlerLog.length) { setImmediate(_f); return; } 516 | let log = handlerLog[0]; 517 | assert('ct' === log.context); 518 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 519 | assert('random' === log.cmd); 520 | const _res = log.retVal; 521 | log = handlerLog[1]; 522 | assert('ct' === log.context); 523 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 524 | assert('log' === log.cmd); 525 | // interesting part here: singe random() is async, its callback (cb1) will 526 | // be invoked after callback of (sync) log(), i.e. cb2... 527 | // this way we can verify the request-responses are matched accordingly, 528 | // as first request should be matched with second response, and second 529 | // request with first response... 530 | log = handlerLog[2]; 531 | assert('cb2' === log.cmd); 532 | assert(undefined === log.result); 533 | log = handlerLog[3]; 534 | assert('cb1' === log.cmd); 535 | assert(_res === log.args); 536 | done(); 537 | }); 538 | }); 539 | 540 | it('should query contexts of given tabId only (bg to (multiple) ctx, first response)', (done) => { 541 | const res = bg.cmd(4, 'random', 100, (result) => { addLog(0, 0, 'callback', result); }); 542 | assert(true === res); 543 | setImmediate(function _f() { 544 | if (3 !== handlerLog.length) { setImmediate(_f); return; } 545 | let log = handlerLog[0]; 546 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 547 | assert('random' === log.cmd); 548 | const _res = log.retVal; 549 | const _ctx = log.context; // either 'ct' or 'dt' 550 | log = handlerLog[1]; 551 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 552 | assert('random' === log.cmd); 553 | assert(_ctx !== log.context); // this must be the other one 554 | log = handlerLog[2]; 555 | assert('callback' === log.cmd); 556 | assert(_res === log.args); // we only get first response back 557 | done(); 558 | }); 559 | }); 560 | 561 | it('should query contexts of given tabId only (bg to (multiple) ctx, all responses)', (done) => { 562 | const res = bg.bcast(4, 'random', 100, (result) => { addLog(0, 0, 'callback', result); }); 563 | assert(true === res); 564 | setImmediate(function _f() { 565 | if (3 !== handlerLog.length) { setImmediate(_f); return; } 566 | let log = handlerLog[0]; 567 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 568 | assert('random' === log.cmd); 569 | const resps = []; 570 | resps.push(log.retVal); 571 | const _ctx = log.context; // either 'ct' or 'dt' 572 | log = handlerLog[1]; 573 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 574 | assert('random' === log.cmd); 575 | assert(_ctx !== log.context); // this must be the other one 576 | resps.push(log.retVal); 577 | log = handlerLog[2]; 578 | assert('callback' === log.cmd); 579 | const _resps = log.args; // it should be an array with all the results 580 | _resps.sort((a, b) => a - b); 581 | resps.sort((a, b) => a - b); 582 | assert(resps.length === _resps.length); // the result arrays are the same 583 | for (let i = 0; i < resps.length; i += 1) { 584 | assert(resps[i] === _resps[i]); 585 | } 586 | done(); 587 | }); 588 | }); 589 | 590 | it('should query contexts of (single) given context type only (bg to (multiple) ctx, all responses)', (done) => { 591 | const res = bg.bcast(['dt'], 'random', 100, (result) => { addLog(0, 0, 'callback', result); }); 592 | assert(true === res); 593 | setImmediate(function _f() { 594 | if (9 !== handlerLog.length) { setImmediate(_f); return; } // 8 x dt + cb 595 | const resps = []; 596 | const idxs = []; 597 | let i; 598 | let log; 599 | for (i = 0; i < 8; i += 1) { 600 | log = handlerLog[i]; 601 | assert('random' === log.cmd); 602 | assert('dt' === log.context); 603 | resps.push(log.retVal); 604 | idxs.push(log.idx); 605 | } 606 | idxs.sort((a, b) => a - b); 607 | resps.sort((a, b) => a - b); 608 | for (i = 0; i < 8; i += 1) { assert(i + 1 === idxs[i]); } // all dt contexts 609 | log = handlerLog[8]; // callback 610 | assert('callback' === log.cmd); 611 | const _resps = log.args; // it should be an array with all the results 612 | _resps.sort((a, b) => a - b); 613 | assert(resps.length === _resps.length); // the result arrays are the same 614 | for (i = 0; i < resps.length; i += 1) { 615 | assert(resps[i] === _resps[i]); 616 | } 617 | done(); 618 | }); 619 | }); 620 | 621 | it('should query contexts of (multiple) given context types only (bg to (multiple) ctx, all responses)', (done) => { 622 | const res = bg.bcast(['dt', 'ct'], 'random', 100, (result) => { addLog(0, 0, 'callback', result); }); 623 | assert(true === res); 624 | setImmediate(function _f() { 625 | if (17 !== handlerLog.length) { setImmediate(_f); return; } // 8 x ct + 8 x dt + cb 626 | const resps = []; 627 | const idxs = []; 628 | let i; 629 | let log; 630 | for (i = 0; i < 16; i += 1) { 631 | log = handlerLog[i]; 632 | assert('random' === log.cmd); 633 | assert(('dt' === log.context) || ('ct' === log.context)); 634 | resps.push(log.retVal); 635 | idxs.push(log.idx); 636 | } 637 | idxs.sort((a, b) => a - b); 638 | resps.sort((a, b) => a - b); 639 | for (i = 0; i < 16; i += 1) { 640 | assert(1 + Math.floor(i / 2) === idxs[i]); // all ct/dt contexts 641 | } 642 | log = handlerLog[16]; // callback 643 | assert('callback' === log.cmd); 644 | const _resps = log.args; // it should be an array with all the results 645 | _resps.sort((a, b) => a - b); 646 | assert(resps.length === _resps.length); // the result arrays are the same 647 | for (i = 0; i < resps.length; i += 1) { 648 | assert(resps[i] === _resps[i]); 649 | } 650 | done(); 651 | }); 652 | }); 653 | 654 | it('should query all contexts (bg to (all) ctx, all responses)', (done) => { 655 | const res = bg.bcast('random', 100, (result) => { addLog(0, 0, 'callback', result); }); 656 | assert(true === res); 657 | setImmediate(function _f() { 658 | if (21 !== handlerLog.length) { setImmediate(_f); return; } // 20 x ctx + cb 659 | const resps = []; 660 | const idxs = []; 661 | let i; 662 | let log; 663 | for (i = 0; i < 20; i += 1) { 664 | log = handlerLog[i]; 665 | assert('random' === log.cmd); 666 | resps.push(log.retVal); 667 | idxs.push(log.idx); 668 | } 669 | idxs.sort((a, b) => a - b); 670 | resps.sort((a, b) => a - b); 671 | for (i = 0; i < 20; i += 1) { assert(1 + Math.floor(i / 2) === idxs[i]); } // all contexts 672 | log = handlerLog[20]; // callback 673 | assert('callback' === log.cmd); 674 | const _resps = log.args; // it should be an array with all the results 675 | _resps.sort((a, b) => a - b); 676 | assert(resps.length === _resps.length); // the result arrays are the same 677 | for (i = 0; i < resps.length; i += 1) { 678 | assert(resps[i] === _resps[i]); 679 | } 680 | done(); 681 | }); 682 | }); 683 | 684 | it('should invoke callback when the requested handler is not found in any context (bg to (all) ctx, single response)', (done) => { 685 | const res = bg.cmd('__random__', 100, (result) => { addLog(0, 0, 'callback', result); }); 686 | assert(true === res); 687 | setImmediate(function _f() { 688 | if (1 !== handlerLog.length) { setImmediate(_f); return; } 689 | const log = handlerLog[0]; 690 | assert('callback' === log.cmd); 691 | assert(undefined === log.args); 692 | done(); 693 | }); 694 | }); 695 | 696 | it('should invoke callback when the requested handler is not found in any context (bg to (all) ctx, all responses)', (done) => { 697 | const res = bg.bcast('__random__', 100, (result) => { addLog(0, 0, 'callback', result); }); 698 | assert(true === res); 699 | setImmediate(function _f() { 700 | if (1 !== handlerLog.length) { setImmediate(_f); return; } 701 | const log = handlerLog[0]; 702 | assert('callback' === log.cmd); 703 | assert(0 === log.args.length); 704 | done(); 705 | }); 706 | }); 707 | 708 | it('should ignore responses with invalid return values (bg to (all) ctx, single response)', (done) => { 709 | const res = bg.cmd('popup_10_cmd', (result) => { addLog(0, 0, 'callback', result); }); 710 | assert(true === res); 711 | setImmediate(function _f() { 712 | if (2 !== handlerLog.length) { setImmediate(_f); return; } // popup_10 + cb 713 | let log = handlerLog[0]; 714 | assert('popup' === log.context); 715 | assert('popup_10_cmd' === log.cmd); 716 | assert(10 === log.idx); 717 | log = handlerLog[1]; 718 | assert('callback' === log.cmd); 719 | assert('hello world' === log.args); 720 | done(); 721 | }); 722 | }); 723 | 724 | it('should ignore responses with invalid return values (bg to (all) ctx, all responses)', (done) => { 725 | const res = bg.bcast('options_cmd', 'message', (result) => { addLog(0, 0, 'callback', result); }); 726 | assert(true === res); 727 | setImmediate(function _f() { 728 | if (3 !== handlerLog.length) { setImmediate(_f); return; } // 2 x options + cb 729 | let log; 730 | let i; 731 | for (i = 0; i < 2; i += 1) { 732 | log = handlerLog[i]; 733 | assert('options' === log.context); 734 | assert('options_cmd' === log.cmd); 735 | } 736 | log = handlerLog[2]; 737 | assert('callback' === log.cmd); 738 | assert(2 === log.args.length); 739 | for (i = 0; i < 2; i += 1) { assert('message' === log.args[i]); } 740 | done(); 741 | }); 742 | }); 743 | 744 | it('should query contexts of given tabId only (ctx to (multiple) ctx, first response)', (done) => { 745 | const res = ctx[0].cmd(4, 'random', 100, (result) => { addLog(0, 0, 'callback', result); }); 746 | assert(true === res); 747 | setImmediate(function _f() { 748 | if (3 !== handlerLog.length) { setImmediate(_f); return; } 749 | let log = handlerLog[0]; 750 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 751 | assert('random' === log.cmd); 752 | const _res = log.retVal; 753 | const _ctx = log.context; // either 'ct' or 'dt' 754 | log = handlerLog[1]; 755 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 756 | assert('random' === log.cmd); 757 | assert(_ctx !== log.context); // this must be the other one 758 | log = handlerLog[2]; 759 | assert('callback' === log.cmd); 760 | assert(_res === log.args); // we only get first response back 761 | done(); 762 | }); 763 | }); 764 | 765 | it('should query contexts of given tabId only (ctx to (multiple) ctx, all responses)', (done) => { 766 | const res = ctx[0].bcast(4, 'random', 100, (result) => { addLog(0, 0, 'callback', result); }); 767 | assert(true === res); 768 | setImmediate(function _f() { 769 | if (3 !== handlerLog.length) { setImmediate(_f); return; } 770 | let log = handlerLog[0]; 771 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 772 | assert('random' === log.cmd); 773 | const resps = []; 774 | resps.push(log.retVal); 775 | const _ctx = log.context; // either 'ct' or 'dt' 776 | log = handlerLog[1]; 777 | assert(3 === log.idx); // on tabId:4 there is ct with idx:3 778 | assert('random' === log.cmd); 779 | assert(_ctx !== log.context); // this must be the other one 780 | resps.push(log.retVal); 781 | log = handlerLog[2]; 782 | assert('callback' === log.cmd); 783 | const _resps = log.args; // it should be an array with all the results 784 | _resps.sort((a, b) => a - b); 785 | resps.sort((a, b) => a - b); 786 | assert(resps.length === _resps.length); // the result arrays are the same 787 | for (let i = 0; i < resps.length; i += 1) { 788 | assert(resps[i] === _resps[i]); 789 | } 790 | done(); 791 | }); 792 | }); 793 | 794 | it('should query contexts of (single) given context type only (ctx to (multiple) ctx, all responses)', (done) => { 795 | const res = ctx[1].bcast(['dt'], 'random', 100, (result) => { addLog(0, 0, 'callback', result); }); 796 | assert(true === res); 797 | setImmediate(function _f() { 798 | if (8 !== handlerLog.length) { setImmediate(_f); return; } // 8 x dt - invoking dt + cb 799 | const resps = []; 800 | const idxs = []; 801 | let i; 802 | let log; 803 | for (i = 0; i < 7; i += 1) { 804 | log = handlerLog[i]; 805 | assert('random' === log.cmd); 806 | assert('dt' === log.context); 807 | resps.push(log.retVal); 808 | idxs.push(log.idx); 809 | } 810 | idxs.sort((a, b) => a - b); 811 | resps.sort((a, b) => a - b); 812 | for (i = 0; i < 7; i += 1) { assert(i + 2 === idxs[i]); } // 8 x dt - invoking dt 813 | log = handlerLog[7]; // callback 814 | assert('callback' === log.cmd); 815 | const _resps = log.args; // it should be an array with all the results 816 | _resps.sort((a, b) => a - b); 817 | assert(resps.length === _resps.length); // the result arrays are the same 818 | for (i = 0; i < resps.length; i += 1) { 819 | assert(resps[i] === _resps[i]); 820 | } 821 | done(); 822 | }); 823 | }); 824 | 825 | it('should query contexts of (multiple) given context types only (ctx to (multiple) ctx, all responses)', (done) => { 826 | const res = ctx[1].bcast(['dt', 'ct'], 'random', 100, (result) => { addLog(0, 0, 'callback', result); }); 827 | assert(true === res); 828 | setImmediate(function _f() { 829 | if (16 !== handlerLog.length) { 830 | setImmediate(_f); // 8 x ct + 8 x dt - invoking dt + cb 831 | return; 832 | } 833 | const resps = []; 834 | const idxs = []; 835 | let i; 836 | let log; 837 | for (i = 0; i < 15; i += 1) { 838 | log = handlerLog[i]; 839 | assert('random' === log.cmd); 840 | assert(('dt' === log.context) || ('ct' === log.context)); 841 | resps.push(log.retVal); 842 | idxs.push(log.idx); 843 | } 844 | idxs.sort((a, b) => a - b); 845 | resps.sort((a, b) => a - b); 846 | for (i = 0; i < 15; i += 1) { 847 | assert(1 + Math.floor((i + 1) / 2) === idxs[i]); // all ct/dt contexts - invoking dt 848 | } 849 | log = handlerLog[15]; // callback 850 | assert('callback' === log.cmd); 851 | const _resps = log.args; // it should be an array with all the results 852 | _resps.sort((a, b) => a - b); 853 | assert(resps.length === _resps.length); // the result arrays are the same 854 | for (i = 0; i < resps.length; i += 1) { 855 | assert(resps[i] === _resps[i]); 856 | } 857 | done(); 858 | }); 859 | }); 860 | 861 | it('should query dt context of the SAME_TAB id (ctx to (same-tab) dt ctx, single response)', (done) => { 862 | // ctx[10]: 'ct', idx:5 863 | const res = ctx[10].cmd(ctxMain[10].SAME_TAB, 'random', 100, (result) => { addLog(0, 0, 'callback', result); }); 864 | assert(true === res); 865 | setImmediate(function _f() { 866 | if (2 !== handlerLog.length) { setImmediate(_f); return; } 867 | let log = handlerLog[0]; 868 | assert('dt' === log.context); 869 | assert(5 === log.idx); 870 | assert('random' === log.cmd); 871 | const _res = log.retVal; 872 | log = handlerLog[1]; 873 | assert('callback' === log.cmd); 874 | assert(_res === log.args); 875 | done(); 876 | }); 877 | }); 878 | 879 | it('should query all contexts (ctx to (all) bg+ctx, all responses)', (done) => { 880 | const res = ctx[1].bcast('random', 100, (result) => { addLog(0, 0, 'callback', result); }); 881 | assert(true === res); 882 | setImmediate(function _f() { 883 | if (21 !== handlerLog.length) { 884 | setImmediate(_f); // bg + 20 x ctx - invoking dt + cb 885 | return; 886 | } 887 | const resps = []; 888 | const idxs = []; 889 | let i; 890 | let log; 891 | for (i = 0; i < 20; i += 1) { 892 | log = handlerLog[i]; 893 | assert('random' === log.cmd); 894 | resps.push(log.retVal); 895 | idxs.push(log.idx); 896 | } 897 | idxs.sort((a, b) => a - b); 898 | resps.sort((a, b) => a - b); 899 | for (i = 0; i < 20; i += 1) { 900 | assert(1 + Math.floor(i / 2) === idxs[i]); // bg (idx:1) + all contexts - invoking dt 901 | } 902 | log = handlerLog[20]; // callback 903 | assert('callback' === log.cmd); 904 | const _resps = log.args; // it should be an array with all the results 905 | _resps.sort((a, b) => a - b); 906 | assert(resps.length === _resps.length); // the result arrays are the same 907 | for (i = 0; i < resps.length; i += 1) { 908 | assert(resps[i] === _resps[i]); 909 | } 910 | done(); 911 | }); 912 | }); 913 | 914 | it('should invoke callback when the requested handler is not found in any context (ctx to (all) ctx, single response)', (done) => { 915 | const res = ctx[0].cmd('__random__', 100, (result) => { addLog(0, 0, 'callback', result); }); 916 | assert(true === res); 917 | setImmediate(function _f() { 918 | if (1 !== handlerLog.length) { setImmediate(_f); return; } 919 | const log = handlerLog[0]; 920 | assert('callback' === log.cmd); 921 | assert(undefined === log.args); 922 | done(); 923 | }); 924 | }); 925 | 926 | it('should invoke callback when the requested handler is not found in any context (ctx to (all) ctx, all responses)', (done) => { 927 | const res = ctx[0].bcast('__random__', 100, (result) => { addLog(0, 0, 'callback', result); }); 928 | assert(true === res); 929 | setImmediate(function _f() { 930 | if (1 !== handlerLog.length) { setImmediate(_f); return; } 931 | const log = handlerLog[0]; 932 | assert('callback' === log.cmd); 933 | assert(0 === log.args.length); 934 | done(); 935 | }); 936 | }); 937 | 938 | it('should ignore responses with invalid return values (ctx to (all) ctx, single response)', (done) => { 939 | const res = ctx[4].cmd('popup_10_cmd', (result) => { addLog(0, 0, 'callback', result); }); 940 | assert(true === res); 941 | setImmediate(function _f() { 942 | if (2 !== handlerLog.length) { setImmediate(_f); return; } // popup_10 + cb 943 | let log = handlerLog[0]; 944 | assert('popup' === log.context); 945 | assert('popup_10_cmd' === log.cmd); 946 | assert(10 === log.idx); 947 | log = handlerLog[1]; 948 | assert('callback' === log.cmd); 949 | assert('hello world' === log.args); 950 | done(); 951 | }); 952 | }); 953 | 954 | it('should ignore responses with invalid return values (ctx to (all) ctx, all responses)', (done) => { 955 | const res = ctx[0].bcast('ct_cmd', 'message', (result) => { addLog(0, 0, 'callback', result); }); 956 | assert(true === res); 957 | setImmediate(function _f() { 958 | if (8 !== handlerLog.length) { 959 | setImmediate(_f); // 8 x ct - invoking ct (idx:1) + cb 960 | return; 961 | } 962 | let log; 963 | let i; 964 | for (i = 0; i < 7; i += 1) { 965 | log = handlerLog[i]; 966 | assert('ct' === log.context); 967 | assert('ct_cmd' === log.cmd); 968 | } 969 | log = handlerLog[7]; 970 | assert('callback' === log.cmd); 971 | assert(7 === log.args.length); 972 | for (i = 0; i < 7; i += 1) { assert('message' === log.args[i]); } 973 | done(); 974 | }); 975 | }); 976 | 977 | it('should invoke onDisconnect background handler on Port disconnect', (done) => { 978 | let _port; 979 | _port = ctxMain[19].__getPort(); // 'dt', id: 10 980 | _port.disconnect(); 981 | _port = ctxMain[18].__getPort(); // 'ct', id: 10 982 | _port.disconnect(); 983 | setImmediate(function _f() { 984 | if (2 !== handlerLog.length) { setImmediate(_f); return; } 985 | let log; 986 | log = handlerLog[0]; 987 | assert('bg' === log.context); 988 | assert('onDisconnect' === log.cmd); 989 | assert('dt' === log.args[0]); 990 | assert(10 === log.args[1]); 991 | log = handlerLog[1]; 992 | assert('bg' === log.context); 993 | assert('onDisconnect' === log.cmd); 994 | assert('ct' === log.args[0]); 995 | assert(10 === log.args[1]); 996 | // uninstall onConnect / onDisconnect background handlers 997 | bgHandlers.onConnect = undefined; 998 | bgHandlers.onDisconnect = undefined; 999 | done(); 1000 | }); 1001 | }); 1002 | 1003 | it('should not wait for response when Port is disconnected (bg to (single) ctx, single response)', (done) => { 1004 | const res = bg.cmd(9, ['dt'], 'block', (result) => { addLog(0, 0, 'callback', result); }); 1005 | assert(true === res); 1006 | setImmediate(function _f() { 1007 | if (1 !== handlerLog.length) { setImmediate(_f); return; } 1008 | let log = handlerLog[0]; 1009 | assert('dt' === log.context); 1010 | assert(7 === log.idx); 1011 | assert('block' === log.cmd); 1012 | const pending = bgMain.__getPendingReqs(); 1013 | assert('object' === typeof pending); 1014 | assert(1 === Object.keys(pending).length); // 'dt-18' 1015 | assert('object' === typeof pending['dt-18']); // array 1016 | assert('object' === typeof pending['dt-18'][0]); 1017 | assert(2 === Object.keys(pending['dt-18'][0]).length); // 'cb', 'id' 1018 | assert('function' === typeof pending['dt-18'][0].cb); 1019 | assert('number' === typeof pending['dt-18'][0].id); 1020 | const _port = ctxMain[17].__getPort(); 1021 | _port.disconnect(); 1022 | setImmediate(function _g() { 1023 | if (2 !== handlerLog.length) { setImmediate(_g); return; } 1024 | log = handlerLog[1]; 1025 | assert('callback' === log.cmd); 1026 | assert(undefined === log.args); 1027 | done(); 1028 | }); 1029 | }); 1030 | }); 1031 | 1032 | it('should not wait for response when Port is disconnected (bg to (single) ctx, all responses)', (done) => { 1033 | const res = bg.bcast(9, ['ct'], 'block', (result) => { addLog(0, 0, 'callback', result); }); 1034 | assert(true === res); 1035 | setImmediate(function _f() { 1036 | if (1 !== handlerLog.length) { setImmediate(_f); return; } 1037 | let log = handlerLog[0]; 1038 | assert('ct' === log.context); 1039 | assert(7 === log.idx); 1040 | assert('block' === log.cmd); 1041 | const pending = bgMain.__getPendingReqs(); 1042 | assert('object' === typeof pending); 1043 | assert(1 === Object.keys(pending).length); // 'ct-17' 1044 | assert('object' === typeof pending['ct-17']); // array 1045 | assert('object' === typeof pending['ct-17'][0]); 1046 | assert(2 === Object.keys(pending['ct-17'][0]).length); // 'cb', 'id' 1047 | assert('function' === typeof pending['ct-17'][0].cb); 1048 | assert('number' === typeof pending['ct-17'][0].id); 1049 | const _port = ctxMain[16].__getPort(); 1050 | _port.disconnect(); 1051 | setImmediate(function _g() { 1052 | if (2 !== handlerLog.length) { setImmediate(_g); return; } 1053 | log = handlerLog[1]; 1054 | assert('callback' === log.cmd); 1055 | assert(0 === log.args.length); 1056 | done(); 1057 | }); 1058 | }); 1059 | }); 1060 | 1061 | it('should not wait for response when Port is disconnected (ctx to (single) ctx, single response)', (done) => { 1062 | const res = ctx[0].cmd(8, ['dt'], 'block', (result) => { addLog(0, 0, 'callback', result); }); 1063 | assert(true === res); 1064 | setImmediate(function _f() { 1065 | if (1 !== handlerLog.length) { setImmediate(_f); return; } 1066 | let log = handlerLog[0]; 1067 | assert('dt' === log.context); 1068 | assert(6 === log.idx); 1069 | assert('block' === log.cmd); 1070 | const pending = bgMain.__getPendingReqs(); 1071 | assert('object' === typeof pending); 1072 | assert(1 === Object.keys(pending).length); // 'dt-16' 1073 | assert('object' === typeof pending['dt-16']); // array 1074 | assert('object' === typeof pending['dt-16'][0]); 1075 | assert(2 === Object.keys(pending['dt-16'][0]).length); // 'cb', 'id' 1076 | assert('function' === typeof pending['dt-16'][0].cb); 1077 | assert('number' === typeof pending['dt-16'][0].id); 1078 | const _port = ctxMain[15].__getPort(); 1079 | _port.disconnect(); 1080 | setImmediate(function _g() { 1081 | if (2 !== handlerLog.length) { setImmediate(_g); return; } 1082 | log = handlerLog[1]; 1083 | assert('callback' === log.cmd); 1084 | assert(undefined === log.args); 1085 | done(); 1086 | }); 1087 | }); 1088 | }); 1089 | 1090 | it('should not wait for response when Port is disconnected (ctx to (single) ctx, all responses)', (done) => { 1091 | const res = ctx[0].bcast(8, ['ct'], 'block', (result) => { addLog(0, 0, 'callback', result); }); 1092 | assert(true === res); 1093 | setImmediate(function _f() { 1094 | if (1 !== handlerLog.length) { setImmediate(_f); return; } 1095 | let log = handlerLog[0]; 1096 | assert('ct' === log.context); 1097 | assert(6 === log.idx); 1098 | assert('block' === log.cmd); 1099 | const pending = bgMain.__getPendingReqs(); 1100 | assert('object' === typeof pending); 1101 | assert(1 === Object.keys(pending).length); // 'ct-15' 1102 | assert('object' === typeof pending['ct-15']); // array 1103 | assert('object' === typeof pending['ct-15'][0]); 1104 | assert(2 === Object.keys(pending['ct-15'][0]).length); // 'cb', 'id' 1105 | assert('function' === typeof pending['ct-15'][0].cb); 1106 | assert('number' === typeof pending['ct-15'][0].id); 1107 | const _port = ctxMain[14].__getPort(); 1108 | _port.disconnect(); 1109 | setImmediate(function _g() { 1110 | if (2 !== handlerLog.length) { setImmediate(_g); return; } 1111 | log = handlerLog[1]; 1112 | assert('callback' === log.cmd); 1113 | assert(0 === log.args.length); 1114 | done(); 1115 | }); 1116 | }); 1117 | }); 1118 | 1119 | it('should update portMap in bg context accordingly (6 disconnected Ports)', () => { 1120 | const pm = bgMain.__getPortMap(); 1121 | assert(4 === Object.keys(pm).length); // ct, dt, popup, options 1122 | assert(pm.ct); 1123 | assert(5 === Object.keys(pm.ct).length); // 5 x 'ct' context 1124 | assert(pm.dt); 1125 | assert(5 === Object.keys(pm.dt).length); // 5 x 'dt' context 1126 | assert(pm.popup); 1127 | assert(2 === Object.keys(pm.popup).length); // 2 x 'popup' context 1128 | assert(pm.options); 1129 | assert(2 === Object.keys(pm.options).length); // 2 x 'options' context 1130 | }); 1131 | 1132 | it('should not invoke handlers of disconnected Ports', (done) => { 1133 | const res = bg.bcast('random', 100, (result) => { addLog(0, 0, 'callback', result); }); 1134 | assert(true === res); 1135 | setImmediate(function _f() { 1136 | if (15 !== handlerLog.length) { setImmediate(_f); return; } // 14 ctx + cb 1137 | const log = handlerLog[14]; 1138 | assert('callback' === log.cmd); 1139 | assert(14 === log.args.length); 1140 | done(); 1141 | }); 1142 | }); 1143 | 1144 | it('should properly connect to another extension', (done) => { 1145 | // install onConnect / onDisconnect handlers 1146 | bgHandlers.onExtensionConnect = bgHandlers._onExtensionConnect; 1147 | bgHandlers.onExtensionDisconnect = bgHandlers._onExtensionDisconnect; 1148 | bgExtHandlers.onExtensionConnect = bgExtHandlers._onExtensionConnect; 1149 | bgExtHandlers.onExtensionDisconnect = bgExtHandlers._onExtensionDisconnect; 1150 | 1151 | bg.connectExt(runtime2.id); 1152 | 1153 | setImmediate(function _f() { 1154 | if (handlerLog.length < 2) { setImmediate(_f); return; } 1155 | assert.strictEqual(handlerLog[0].context, 'bg'); 1156 | assert.strictEqual(handlerLog[0].idx, 1); 1157 | assert.strictEqual(handlerLog[0].cmd, 'onExtensionConnect'); 1158 | assert.strictEqual(handlerLog[0].args[0], runtime2.id); 1159 | assert.strictEqual(handlerLog[1].context, 'bg'); 1160 | assert.strictEqual(handlerLog[1].idx, 2); 1161 | assert.strictEqual(handlerLog[1].cmd, 'onExtensionConnect'); 1162 | assert.strictEqual(handlerLog[1].args[0], runtime1.id); 1163 | 1164 | done(); 1165 | }); 1166 | }); 1167 | 1168 | it('should properly call handler of foreign extension\'s background', (done) => { 1169 | const res = bg1.cmdExt(runtime1.id, 'log', true, 0.1, 'str', ['a', 'b'], { o: 1, p: 2 }, null, undefined, 1); 1170 | assert(true === res); 1171 | setImmediate(function _f() { 1172 | if (1 !== handlerLog.length) { setImmediate(_f); return; } 1173 | const log = handlerLog[0]; 1174 | assert('bg' === log.context); 1175 | assert.strictEqual(log.idx, 1); 1176 | assert('log' === log.cmd); 1177 | const args = log.args; 1178 | assert(8 === args.length); 1179 | assert(true === args[0]); 1180 | assert(0.1 === args[1]); 1181 | assert('str' === args[2]); 1182 | assert.deepEqual(['a', 'b'], args[3]); 1183 | assert.deepEqual({ o: 1, p: 2 }, args[4]); 1184 | assert(null === args[5]); 1185 | assert((undefined === args[6]) || (null === args[6])); 1186 | assert(1 === args[7]); 1187 | done(); 1188 | }); 1189 | }); 1190 | }); 1191 | --------------------------------------------------------------------------------