├── 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 |,
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 |
--------------------------------------------------------------------------------