├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── README.md ├── dev ├── App.js └── index.js ├── package.json ├── src ├── TabTalk.js ├── constants.js ├── index.js ├── sessionStorage.js └── utils.js ├── test ├── TabTalk.js ├── helpers │ └── setup-browser-env.js ├── sessionStorage.js └── utils.js └── webpack ├── webpack.config.dev.js ├── webpack.config.js └── webpack.config.minified.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": [ 5 | [ 6 | "env", 7 | { 8 | "loose": true, 9 | "modules": false 10 | } 11 | ], 12 | "stage-2" 13 | ] 14 | }, 15 | "es": { 16 | "presets": [ 17 | [ 18 | "env", 19 | { 20 | "loose": true, 21 | "modules": false 22 | } 23 | ], 24 | "stage-2" 25 | ] 26 | }, 27 | "lib": { 28 | "presets": [ 29 | [ 30 | "env", 31 | { 32 | "loose": true 33 | } 34 | ], 35 | "stage-2" 36 | ] 37 | }, 38 | "production": { 39 | "presets": [ 40 | [ 41 | "env", 42 | { 43 | "loose": true, 44 | "modules": false 45 | } 46 | ], 47 | "stage-2" 48 | ] 49 | }, 50 | "test": { 51 | "plugins": ["transform-runtime"], 52 | "presets": [ 53 | [ 54 | "env", 55 | { 56 | "loose": true 57 | } 58 | ], 59 | "stage-2" 60 | ] 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["rapid7/browser", "rapid7/react"], 3 | "globals": { 4 | "__dirname": true, 5 | "global": true, 6 | "module": true, 7 | "process": true, 8 | "require": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "rapid7/no-trailing-underscore": 0, 13 | "no-unused-vars": [ 14 | 1, 15 | { 16 | "ignoreRestSiblings": true 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .git 4 | .nyc_output 5 | coverage 6 | dist 7 | es 8 | node_modules 9 | lib 10 | *.log 11 | *.lock 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .git 3 | .nyc_output 4 | coverage 5 | dev 6 | mocks 7 | src 8 | test 9 | .babelrc 10 | .editorconfig 11 | .eslintrc 12 | .gitignore 13 | .yarnrc 14 | mock-err.log 15 | mock-out.log 16 | webpack 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tabtalk 2 | 3 | Simple, secure cross-origin communication between browser tabs 4 | 5 | ## Table of contents 6 | 7 | - [Usage](#usage) 8 | - [createTab](#createtab) 9 | - [config](#config) 10 | - [TabTalk instance API](#tabtalk-instance-api) 11 | - [open](#open) 12 | - [close](#close) 13 | - [sendToChild](#sendtochild) 14 | - [sendToChildren](#sendtochildren) 15 | - [sendToParent](#sendtoparent) 16 | 17 | ## Usage 18 | 19 | ```javascript 20 | import createTab from 'tabtalk'; 21 | 22 | const tabtalk = createTab({ 23 | onChildCommunication(message) { 24 | console.log('message from child', message); 25 | }, 26 | onChildRegister(childTab) { 27 | tabtalk.sendToChild(childTab.id, 'Hello my child!'); 28 | }, 29 | onRegister() { 30 | console.log('ready to go!'); 31 | }, 32 | }); 33 | 34 | tabtalk.open({url: 'http://www.example.com'}); 35 | ``` 36 | 37 | Messages are sent by `postMessage`, allowing for cross-origin usage (with the use of custom `config` properties), and the data in the message is encrypted with `krip` under the hood. 38 | 39 | ## createTab 40 | 41 | ```javascript 42 | createTab([config: Object]): TabTalk 43 | ``` 44 | 45 | Creates a new `TabTalk` instance where the window it is executed in (itself) is the tab. The instance returned allows you to open children, and will automatically glean its parent if it was opened from another `TabTalk` instance. 46 | 47 | #### config 48 | 49 | `createTab` optionally accepts a `config` object which has the following shape: 50 | 51 | ```javascript 52 | { 53 | // custom key to be used by krip for encryption / decription 54 | // HIGHLY RECOMMENDED 55 | encryptionKey: string, 56 | 57 | // called when child closes 58 | onChildClose: (child: TabTalk) => void, 59 | 60 | // called when child sends message to self 61 | onChildCommunication: (message: any, eventTime: number) => void, 62 | 63 | // called when child registers with parent 64 | onChildRegister: (child: TabTalk) => void, 65 | 66 | // called when self is closed 67 | onClose: () => void, 68 | 69 | // called when parent is closed 70 | onParentClose: () => void, 71 | 72 | // called when parent sends message to self 73 | onParentCommunication: (message: any, eventTime: number) => void, 74 | 75 | // called when self registers 76 | onRegister: (self: TabTalk) => void, 77 | 78 | // the origin to use for cross-tab validation 79 | origin: string = window.origin || document.domain || '*', 80 | 81 | // the delay to wait when a tab doesn't check in but has already checked in 82 | pingCheckinBuffer: number = 5000, 83 | 84 | // the delay to wait before the next check if tab is checking in 85 | pingInterval: number = 5000, 86 | 87 | // the delay to wait when a tab doesn't check in and has never checked in 88 | registrationBuffer: number = 10000, 89 | 90 | // should the tab be removed as a child when closed 91 | removeOnClose: boolean = false, 92 | } 93 | ``` 94 | 95 | ## TabTalk instance API 96 | 97 | #### open 98 | 99 | Opens a new child `TabTalk` instance and stores it as a child in the opener's `TabTalk` instance. Resolves the child instance. 100 | 101 | ```javascript 102 | open(options: Object): Promise 103 | ``` 104 | 105 | Example: 106 | 107 | ```javascript 108 | const openChild = async config => { 109 | const child = await tabtalk.open({ 110 | config, 111 | url: 'http://www.example.com/', 112 | }); 113 | 114 | return child; 115 | }; 116 | ``` 117 | 118 | `open()` accepts the following options: 119 | 120 | ```javascript 121 | { 122 | // the config to pass through to createTab for the child 123 | config: Object = self.config, 124 | 125 | // the destination to open 126 | url: string, 127 | 128 | // any windowFeatures to apply to the opened window 129 | windowFeatures: string, 130 | } 131 | ``` 132 | 133 | For more information on `windowFeatures`, [see the MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/open#Parameters). 134 | 135 | #### close 136 | 137 | Closes the `TabTalk` instance, or if an ID is passed then closes that matching child `TabTalk` instance. 138 | 139 | ```javascript 140 | close([id: string]) => void 141 | ``` 142 | 143 | Example: 144 | 145 | ```javascript 146 | tabtalk.close(); // closes self 147 | 148 | tabtalk.close(child.id); // closes specific child 149 | ``` 150 | 151 | #### sendToChild 152 | 153 | Send data to a specific child. The promise resolved is empty, it's mainly to have control flow over a successful message sent. 154 | 155 | ```javascript 156 | sendToChild(id: string, data: any): Promise 157 | ``` 158 | 159 | Example: 160 | 161 | ```javascript 162 | tabtalk.sendToChild(child.id, { 163 | message: 'Special message', 164 | total: 1234.56, 165 | }); 166 | ``` 167 | 168 | **NOTE**: Data can be anything that is serializable by `JSON.stringify`. 169 | 170 | #### sendToChildren 171 | 172 | Send data to all children. The promise resolved is empty, it's mainly to have control flow over a successful message sent. 173 | 174 | ```javascript 175 | sendToChildren(data: any): Promise 176 | ``` 177 | 178 | Example: 179 | 180 | ```javascript 181 | tabtalk.sendToChildren({ 182 | message: 'Special message', 183 | total: 1234.56, 184 | }); 185 | ``` 186 | 187 | **NOTE**: Data can be anything that is serializable by `JSON.stringify`. 188 | 189 | #### sendToParent 190 | 191 | Send data to the parent. The promise resolved is empty, it's mainly to have control flow over a successful message sent. 192 | 193 | ```javascript 194 | sendToParent(data: any): Promise 195 | ``` 196 | 197 | Example: 198 | 199 | ```javascript 200 | tabtalk.sendToParent({ 201 | message: 'Special message', 202 | total: 1234.56, 203 | }); 204 | ``` 205 | 206 | **NOTE**: Data can be anything that is serializable by `JSON.stringify`. 207 | 208 | ## Support 209 | 210 | Support is mainly driven by `krip` support, as `postMessage` works correctly back to IE8. 211 | 212 | - Chrome 37+ 213 | - Firefox 34+ 214 | - Edge (all versions) 215 | - Opera 24+ 216 | - IE 11+ 217 | - Requires polyfills for `Promise`, `TextDecoder`, and `TextEncoder` 218 | - Safari 7.1+ 219 | - iOS 8+ 220 | - Android 5+ 221 | 222 | ## Development 223 | 224 | Standard stuff, clone the repo and `npm install` dependencies. The npm scripts available: 225 | 226 | - `build` => run `rollup` to build browser and node versions 227 | - standard versions to top-level directory, minified versions to `dist` folder 228 | - `clean:dist` => run `rimraf` on `dist` folder 229 | - `dev` => run `webpack` dev server to run example app / playground 230 | - `dist` => run `clean:dist`, `build` 231 | - `lint` => run `eslint` against all files in the `src` folder 232 | - `lint:fix` => run `lint`, fixing issues when possible 233 | - `prepublish` => runs `prepublish:compile` when publishing 234 | - `prepublish:compile` => run `lint`, `test:coverage`, `build` 235 | - `start` => run `dev` 236 | - `test` => run AVA test functions with `NODE_ENV=test` 237 | - `test:coverage` => run `test` but with `nyc` for coverage checker 238 | - `test:watch` => run `test`, but with persistent watcher 239 | -------------------------------------------------------------------------------- /dev/App.js: -------------------------------------------------------------------------------- 1 | // src 2 | import createTab from '../src'; 3 | 4 | const tab = createTab({ 5 | encryptionKey: 'CUSTOM_ENCRYPTION_KEY', 6 | onChildClose(child) { 7 | console.log('child closed', child); 8 | }, 9 | onChildCommunication(message) { 10 | console.log('child communication', message); 11 | }, 12 | async onChildRegister(child) { 13 | console.log('child registered', child); 14 | 15 | // await new Promise((resolve) => setTimeout(resolve, 3000)); 16 | 17 | // tab.close(child.id); 18 | }, 19 | onParentClose() { 20 | console.log('parent closed'); 21 | }, 22 | onParentCommunication(message) { 23 | console.log('parent communication', message); 24 | }, 25 | onRegister(registeredTab) { 26 | console.log('registered', registeredTab); 27 | }, 28 | }); 29 | 30 | console.log(tab); 31 | 32 | let createdChild; 33 | 34 | const sendDelayedCommunication = async (child) => { 35 | await new Promise((resolve) => setTimeout(resolve, 3000)); 36 | 37 | if (tab.parent) { 38 | console.log('sending to parent'); 39 | 40 | tab.sendToParent({ 41 | id: tab.id, 42 | message: 'I am your child!', 43 | }); 44 | } 45 | 46 | if (child) { 47 | console.log('sending to child', child.id); 48 | 49 | tab.sendToChild(child.id, { 50 | id: tab.id, 51 | message: 'I am your parent!', 52 | }); 53 | } 54 | }; 55 | 56 | const createChild = async () => { 57 | createdChild = await tab.open({url: 'http://localhost:3000'}); 58 | 59 | console.log(tab); 60 | console.log(tab.children); 61 | 62 | sendDelayedCommunication(createdChild); 63 | }; 64 | 65 | const button = document.createElement('button'); 66 | 67 | button.textContent = 'Click to open child'; 68 | button.onclick = createChild; 69 | 70 | document.body.appendChild(button); 71 | 72 | sendDelayedCommunication(tab.children[0]); 73 | -------------------------------------------------------------------------------- /dev/index.js: -------------------------------------------------------------------------------- 1 | import './App'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "tony_quetano@rapid7.com", 3 | "ava": { 4 | "babel": "inherit", 5 | "failFast": true, 6 | "files": [ 7 | "test/**/*.js" 8 | ], 9 | "require": [ 10 | "babel-register", 11 | "./test/helpers/setup-browser-env.js" 12 | ], 13 | "source": [ 14 | "src/**/*.js" 15 | ], 16 | "verbose": true 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/rapid7/tabtalk/issues" 20 | }, 21 | "dependencies": { 22 | "krip": "^1.0.0", 23 | "unchanged": "^1.3.3", 24 | "uuid": "^3.3.2" 25 | }, 26 | "description": "Secure, encrypted cross-tab communication in the browser", 27 | "devDependencies": { 28 | "@trust/webcrypto": "^0.9.2", 29 | "ava": "^0.25.0", 30 | "babel-cli": "^6.26.0", 31 | "babel-core": "^6.26.3", 32 | "babel-eslint": "^8.2.5", 33 | "babel-loader": "^7.1.5", 34 | "babel-plugin-transform-runtime": "^6.23.0", 35 | "babel-preset-env": "^1.7.0", 36 | "babel-preset-stage-2": "^6.24.1", 37 | "babel-register": "^6.26.0", 38 | "babel-runtime": "^6.26.0", 39 | "browser-env": "^3.2.5", 40 | "enzyme": "^3.3.0", 41 | "enzyme-adapter-react-16": "^1.1.1", 42 | "enzyme-to-json": "^3.3.4", 43 | "eslint": "^5.1.0", 44 | "eslint-config-rapid7": "^3.0.3", 45 | "eslint-friendly-formatter": "^4.0.1", 46 | "eslint-loader": "^2.0.0", 47 | "file-loader": "^1.1.11", 48 | "html-webpack-plugin": "^3.2.0", 49 | "image-webpack-loader": "4.3.1", 50 | "in-publish": "^2.0.0", 51 | "mock-webstorage": "^1.0.4", 52 | "nyc": "^12.0.2", 53 | "optimize-js-plugin": "^0.0.4", 54 | "prop-types": "^15.6.2", 55 | "raf": "^3.4.0", 56 | "react": "^16.4.1", 57 | "react-dom": "^16.4.1", 58 | "react-test-renderer": "^16.4.1", 59 | "rimraf": "^2.6.2", 60 | "sinon": "^6.1.3", 61 | "webpack": "^4.17.0", 62 | "webpack-cli": "^3.0.8", 63 | "webpack-dev-server": "^3.1.4" 64 | }, 65 | "homepage": "https://github.com/rapid7/tabtalk#readme", 66 | "keywords": [ 67 | "tabs", 68 | "communication", 69 | "postmessage", 70 | "crosstab", 71 | "cross-tab", 72 | "across-tabs" 73 | ], 74 | "license": "MIT", 75 | "main": "lib/index.js", 76 | "module": "es/index.js", 77 | "name": "tabtalk", 78 | "repository": { 79 | "type": "git", 80 | "url": "git+https://github.com/rapid7/tabtalk.git" 81 | }, 82 | "scripts": { 83 | "build": "NODE_ENV=development webpack --colors --profile --progress --config=webpack/webpack.config.js", 84 | "build:minified": "NODE_ENV=production webpack --colors --profile --progress --config=webpack/webpack.config.minified.js", 85 | "clean": "rimraf lib && rimraf es && rimraf dist", 86 | "dev": "NODE_ENV=development webpack-dev-server --colors --progress --profile --config=webpack/webpack.config.dev.js", 87 | "lint": "eslint --max-warnings 0 src", 88 | "lint:fix": "eslint src --fix", 89 | "prepublish": "if in-publish; then npm run prepublish:compile; fi", 90 | "prepublish:compile": "npm run lint && npm run test:coverage && npm run clean && npm run transpile:lib && npm run transpile:es && npm run build && npm run build:minified", 91 | "start": "npm run dev", 92 | "test": "NODE_PATH=. NODE_ENV=test ava", 93 | "test:coverage": "nyc --cache npm test", 94 | "test:update": "NODE_PATH=. NODE_ENV=test ava --no-cache --update-snapshots", 95 | "test:watch": "NODE_PATH=. NODE_ENV=test ava --watch", 96 | "transpile:es": "BABEL_ENV=es babel src --out-dir es", 97 | "transpile:lib": "BABEL_ENV=lib babel src --out-dir lib" 98 | }, 99 | "version": "1.0.4" 100 | } 101 | -------------------------------------------------------------------------------- /src/TabTalk.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | import { 3 | decrypt, 4 | encrypt 5 | } from 'krip'; 6 | import { 7 | call, 8 | get, 9 | getOr 10 | } from 'unchanged'; 11 | import uuid from 'uuid/v4'; 12 | 13 | // constants 14 | import { 15 | DEFAULT_CONFIG, 16 | EVENT, 17 | SESSION_STORAGE_KEY, 18 | TAB_REFERENCE_KEY, 19 | TAB_STATUS 20 | } from './constants'; 21 | 22 | // storage 23 | import { 24 | getStorageData, 25 | removeStorageData, 26 | setStorageData 27 | } from './sessionStorage'; 28 | 29 | // utils 30 | import { 31 | assign, 32 | findChildTab, 33 | filter, 34 | map, 35 | getChildWindowName, 36 | getHasTimedOut 37 | } from './utils'; 38 | 39 | const getEncryptionKey = get(['encryptionKey']); 40 | const getId = get(['id']); 41 | const getOrigin = get(['origin']); 42 | const getPingInterval = get(['pingInterval']); 43 | const getRemoveOnClosed = get(['removeOnClosed']); 44 | 45 | /** 46 | * @constant {Object|null} EXISTING_TAB the tab already existing on the window 47 | */ 48 | const EXISTING_TAB = getOr(null, [TAB_REFERENCE_KEY], window); 49 | 50 | /** 51 | * @class TabTalk 52 | * 53 | * @classdesc 54 | * manager that allows for communication both with opened children and with parents that opened it 55 | */ 56 | class TabTalk { 57 | /** 58 | * @function constructor 59 | * 60 | * @description 61 | * build a new tab instance, assigning internal values and kicking off listeners when appropriate 62 | * 63 | * @param {Object} config the configuration object 64 | * @param {Window} [ref=window] the reference to the tab's own window 65 | * @param {string} [windowName=window.name] the name for the given window 66 | * @returns {TabTalk} the tab instance 67 | */ 68 | constructor(config, {id, ref = window, windowName = window.name}) { 69 | this.__children = []; 70 | this.config = assign({}, DEFAULT_CONFIG, config); 71 | this.created = Date.now(); 72 | this.lastCheckin = null; 73 | this.lastParentCheckin = null; 74 | this.parent = ref.opener || null; 75 | this.receivePingInterval = null; 76 | this.sendPingInterval = null; 77 | this.ref = ref; 78 | this.status = TAB_STATUS.OPEN; 79 | this.windowName = config.windowName || windowName || `${SESSION_STORAGE_KEY}:ROOT`; 80 | 81 | window.name = this.windowName; 82 | 83 | const currentStorageData = getStorageData(this.windowName); 84 | 85 | this.id = id || getId(EXISTING_TAB) || getId(currentStorageData) || uuid(); 86 | 87 | removeStorageData(this.windowName); 88 | 89 | this.__clearPingIntervals(); 90 | this.__setSendPingInterval(); 91 | 92 | if (ref === window) { 93 | this.__addEventListeners(); 94 | this.__register(); 95 | this.__setReceivePingInterval(); 96 | } 97 | } 98 | 99 | /** 100 | * @type {Array} 101 | */ 102 | get children() { 103 | return this.__children.slice(0); 104 | } 105 | 106 | /** 107 | * @type {Array} 108 | */ 109 | get closedChildren() { 110 | return filter(({status}) => status === TAB_STATUS.CLOSED, this.__children); 111 | } 112 | 113 | /** 114 | * @type {Array} 115 | */ 116 | get openChildren() { 117 | return filter(({status}) => status === TAB_STATUS.OPEN, this.__children); 118 | } 119 | 120 | /** 121 | * @function __addChild 122 | * @memberof TabTalk 123 | * 124 | * @private 125 | * 126 | * @description 127 | * add the child to the list of stored children 128 | * 129 | * @param {TabTalk} child the child tab 130 | */ 131 | __addChild(child) { 132 | this.__children.push(child); 133 | } 134 | 135 | /** 136 | * @function __addEventListeners 137 | * @memberof TabTalk 138 | * 139 | * @private 140 | * 141 | * @description 142 | * add the event listeners for both messaging and unload / closure of tabs 143 | */ 144 | __addEventListeners() { 145 | call( 146 | ['addEventListener'], 147 | [ 148 | 'beforeunload', 149 | () => { 150 | setStorageData(this.windowName, { 151 | id: this.id, 152 | }); 153 | 154 | this.__clearPingIntervals(); 155 | 156 | if (this.parent) { 157 | this.__sendToParent(EVENT.SET_TAB_STATUS, TAB_STATUS.CLOSED); 158 | } 159 | 160 | call(['onClose'], [], this.config); 161 | }, 162 | ], 163 | this.ref 164 | ); 165 | 166 | call(['addEventListener'], ['message', this.__handleMessage], this.ref); 167 | 168 | call(['addEventListener'], ['beforeunload', () => call(['onParentClose'], [], this.config)], this.parent); 169 | } 170 | 171 | /** 172 | * @function __clearPingIntervals 173 | * @memberof TabTalk 174 | * 175 | * @private 176 | * 177 | * @description 178 | * clear the ping intervals 179 | */ 180 | __clearPingIntervals() { 181 | clearInterval(this.receivePingInterval); 182 | clearInterval(this.sendPingInterval); 183 | } 184 | 185 | /** 186 | * @function __handleOnChildCommunicationMessage 187 | * @memberof TabTalk 188 | * 189 | * @private 190 | * 191 | * @description 192 | * handle messages from child tabs 193 | * 194 | * @param {TabTalk} [child] the child tab sending the message 195 | * @param {any} data the data send in the message 196 | * @returns {any} the message data 197 | */ 198 | __handleOnChildCommunicationMessage({child, data}) { 199 | return child && call(['onChildCommunication'], [data, Date.now()], this.config); 200 | } 201 | 202 | /** 203 | * @function __handleOnParentCommunicationMessage 204 | * @memberof TabTalk 205 | * 206 | * @private 207 | * 208 | * @description 209 | * handle messages from the parent tab 210 | * 211 | * @param {string} childId the id of the child sending the message 212 | * @param {any} data the data send in the message 213 | * @returns {any} the message data 214 | */ 215 | __handleOnParentCommunicationMessage({childId, data}) { 216 | return childId === this.id && call(['onParentCommunication'], [data, Date.now()], this.config); 217 | } 218 | 219 | /** 220 | * @function __handlePingChildMessage 221 | * @memberof TabTalk 222 | * 223 | * @private 224 | * 225 | * @description 226 | * handle pings from the parent tab 227 | * 228 | * @param {string} childId the id of the child sending the message 229 | * @param {number} lastParentCheckin the epoch of the last checkin time 230 | * @returns {void} 231 | */ 232 | __handlePingChildMessage({childId, data: lastParentCheckin}) { 233 | return childId === this.id && (this.lastParentCheckin = lastParentCheckin); 234 | } 235 | 236 | /** 237 | * @function __handlePingParentOrRegisterMessage 238 | * @memberof TabTalk 239 | * 240 | * @private 241 | * 242 | * @description 243 | * handle pings or registration from a child tab, auto-registering if a ping comes 244 | * from a child that does not currently exist 245 | * 246 | * @param {TabTalk} [existingChild] the child sending the ping 247 | * @param {string} id the id of the child tab 248 | * @param {number} lastParentCheckin the epoch of the last checkin time 249 | * @param {Window} source the window of the child tab 250 | * @returns {TabTalk} the child tab 251 | */ 252 | __handlePingParentOrRegisterMessage({child: existingChild, id, lastCheckin, source}) { 253 | const child = 254 | existingChild 255 | || new TabTalk(this.config, { 256 | id, 257 | ref: source, 258 | windowName: getChildWindowName(id, this.id), 259 | }); 260 | 261 | if (child !== existingChild) { 262 | this.__addChild(child); 263 | } 264 | 265 | if (!child.lastCheckin) { 266 | call(['onChildRegister'], [child], this.config); 267 | } 268 | 269 | return (child.lastCheckin = lastCheckin) && child; 270 | } 271 | 272 | /** 273 | * @function __handleSetStatusMessage 274 | * @memberof TabTalk 275 | * 276 | * @private 277 | * 278 | * @description 279 | * handle the setting of tab status 280 | * 281 | * @param {TabTalk} child the child tab 282 | * @param {string} status the new status of the tab 283 | */ 284 | __handleSetStatusMessage({child, status}) { 285 | if (!child) { 286 | return; 287 | } 288 | 289 | child.status = status; 290 | 291 | if (status === TAB_STATUS.CLOSED) { 292 | call(['onChildClose'], [child], this.config); 293 | 294 | if (getRemoveOnClosed(this.config)) { 295 | this.__removeChild(child); 296 | } 297 | } 298 | } 299 | 300 | /** 301 | * @function __handleMessage 302 | * @memberof TabTalk 303 | * 304 | * @private 305 | * 306 | * @description 307 | * handle the message from postMessage 308 | * 309 | * @param {string} data the raw data from the message 310 | * @param {string} origin the origin of the message (must match that of the tab configuration) 311 | * @param {Window} source the window of the source tab 312 | * @returns {any} 313 | */ 314 | __handleMessage = ({data: eventData, origin, source}) => { 315 | try { 316 | const {data, event, id} = JSON.parse(eventData); 317 | 318 | if (origin !== getOrigin(this.config) || !event || event !== EVENT[event]) { 319 | return; 320 | } 321 | 322 | const child = findChildTab(this.__children, id); 323 | 324 | return ( 325 | decrypt(data, getEncryptionKey(this.config)) 326 | .then((decrypted) => { 327 | switch (event) { 328 | case EVENT.PING_CHILD: 329 | return this.__handlePingChildMessage(decrypted); 330 | 331 | case EVENT.PING_PARENT: 332 | case EVENT.REGISTER: 333 | return this.__handlePingParentOrRegisterMessage({ 334 | child, 335 | id, 336 | lastCheckin: decrypted, 337 | source, 338 | }); 339 | 340 | case EVENT.SET_TAB_STATUS: 341 | return this.__handleSetStatusMessage({ 342 | child, 343 | status: decrypted, 344 | }); 345 | 346 | case EVENT.CHILD_COMMUNICATION: 347 | return this.__handleOnChildCommunicationMessage({ 348 | child, 349 | data: decrypted, 350 | }); 351 | 352 | case EVENT.PARENT_COMMUNICATION: 353 | return this.__handleOnParentCommunicationMessage(decrypted); 354 | } 355 | }) 356 | // eslint-disable-next-line no-console 357 | .catch((error) => console.error(error)) 358 | ); 359 | } catch (error) { 360 | // ignored 361 | } 362 | }; 363 | 364 | /** 365 | * @function __register 366 | * @memberof TabTalk 367 | * 368 | * @private 369 | * 370 | * @description 371 | * register the tab to the parent 372 | */ 373 | __register() { 374 | if (this.parent) { 375 | this.__sendToParent(EVENT.REGISTER, Date.now()); 376 | this.__sendToParent(EVENT.SET_TAB_STATUS, TAB_STATUS.OPEN); 377 | } 378 | 379 | call(['onRegister'], [this], this.config); 380 | } 381 | 382 | /** 383 | * @function __removeChild 384 | * @memberof TabTalk 385 | * 386 | * @private 387 | * 388 | * @description 389 | * remove the child from the stored children 390 | * 391 | * @param {TabTalk} child the child tab 392 | */ 393 | __removeChild(child) { 394 | this.__children.splice(this.children.indexOf(child), 1); 395 | } 396 | 397 | /** 398 | * @function _sendToChild 399 | * @memberof TabTalk 400 | * 401 | * @private 402 | * 403 | * @description 404 | * send data to a specific child 405 | * 406 | * @param {string} id the id of the child tab 407 | * @param {string} event the tabtalk event 408 | * @param {any} [data=null] the data to send 409 | * @returns {Promise} 410 | */ 411 | __sendToChild(id, event, data = null) { 412 | const child = findChildTab(this.__children, id); 413 | const childId = getId(child); 414 | 415 | return childId 416 | ? child.status === TAB_STATUS.OPEN 417 | ? encrypt( 418 | { 419 | childId, 420 | data, 421 | }, 422 | getEncryptionKey(this.config) 423 | ).then((encrypted) => 424 | child.ref.postMessage( 425 | JSON.stringify({ 426 | data: encrypted, 427 | event, 428 | id: this.id, 429 | }), 430 | getOrigin(this.config) 431 | ) 432 | ) 433 | : Promise.reject(new Error('TabTalk is closed.')) 434 | : Promise.reject(new Error('Child could not be found.')); 435 | } 436 | 437 | /** 438 | * @function __sendToChildren 439 | * @memberof TabTalk 440 | * 441 | * @private 442 | * 443 | * @description 444 | * send data to all children 445 | * 446 | * @param {string} event the tabtalk event 447 | * @param {any} [data=null] the data to send 448 | * @returns {Promise} 449 | */ 450 | __sendToChildren(event, data = null) { 451 | return Promise.all(map(({id}) => this.__sendToChild(id, event, data), this.openChildren)); 452 | } 453 | 454 | /** 455 | * @function __sendToParent 456 | * @memberof TabTalk 457 | * 458 | * @private 459 | * 460 | * @description 461 | * send data to the parent 462 | * 463 | * @param {string} event the tabtalk event 464 | * @param {any} [data=null] the data to send 465 | * @returns {Promise} 466 | */ 467 | __sendToParent(event, data = null) { 468 | return this.parent 469 | ? encrypt(data, getEncryptionKey(this.config)).then((encrypted) => 470 | this.parent.postMessage( 471 | JSON.stringify({ 472 | data: encrypted, 473 | event, 474 | id: this.id, 475 | }), 476 | getOrigin(this.config) 477 | ) 478 | ) 479 | : Promise.reject(new Error('Parent could not be found.')); 480 | } 481 | 482 | /** 483 | * @function __setReceivePingInterval 484 | * @memberof TabTalk 485 | * 486 | * @private 487 | * 488 | * @description 489 | * set the interval to check for pings received from child tabs 490 | */ 491 | __setReceivePingInterval() { 492 | clearInterval(this.receivePingInterval); 493 | 494 | this.receivePingInterval = setInterval( 495 | () => 496 | map( 497 | (child) => 498 | getHasTimedOut(child, this.config) 499 | && this.__handleSetStatusMessage({ 500 | child, 501 | status: TAB_STATUS.CLOSED, 502 | }), 503 | this.openChildren 504 | ), 505 | getPingInterval(this.config) 506 | ); 507 | } 508 | 509 | /** 510 | * @function __setSendPingInterval 511 | * @memberof TabTalk 512 | * 513 | * @private 514 | * 515 | * @description 516 | * set the interval to send pings to children and / or parent 517 | */ 518 | __setSendPingInterval() { 519 | clearInterval(this.sendPingInterval); 520 | 521 | this.sendPingInterval = setInterval(() => { 522 | const lastCheckin = Date.now(); 523 | 524 | this.lastCheckin = lastCheckin; 525 | 526 | if (this.parent) { 527 | this.__sendToParent(EVENT.PING_PARENT, lastCheckin); 528 | } 529 | 530 | if (this.__children.length) { 531 | this.__sendToChildren(EVENT.PING_CHILD, lastCheckin); 532 | } 533 | }, getPingInterval(this.config)); 534 | } 535 | 536 | /** 537 | * @function close 538 | * @memberof TabTalk 539 | * 540 | * @description 541 | * close the child tab with the given id 542 | * 543 | * @param {string} id the id of the tab to close 544 | */ 545 | close(id) { 546 | if (!id) { 547 | return call(['close'], [], this.ref); 548 | } 549 | 550 | const child = findChildTab(this.__children, id); 551 | 552 | if (!child || child.status !== TAB_STATUS.OPEN) { 553 | return; 554 | } 555 | 556 | child.status = TAB_STATUS.CLOSED; 557 | 558 | call(['close'], [], child.ref); 559 | 560 | if (getRemoveOnClosed(this.config)) { 561 | this.__removeChild(child); 562 | } 563 | } 564 | 565 | /** 566 | * @function open 567 | * @memberof TabTalk 568 | * 569 | * @description 570 | * open the tab with the given options 571 | * 572 | * @param {string} url the url to open 573 | * @param {string} windowFeatures the options to open the window with 574 | * @returns {Promise} promise that resolves to the opened tab 575 | */ 576 | open({config = this.config, url, windowFeatures}) { 577 | return new Promise((resolve) => resolve(window.open(url, '_blank', windowFeatures))).then((childWindow) => { 578 | const child = new TabTalk(config, { 579 | ref: childWindow, 580 | }); 581 | 582 | childWindow[TAB_REFERENCE_KEY] = child; 583 | childWindow.name = getChildWindowName(child.id, this.id); 584 | 585 | this.__addChild(child); 586 | 587 | return child; 588 | }); 589 | } 590 | 591 | /** 592 | * @function sendToChild 593 | * @memberof TabTalk 594 | * 595 | * @description 596 | * send data to a specific child 597 | * 598 | * @param {string} id the id of the child tab 599 | * @param {any} [data=null] the data to send 600 | * @returns {Promise} promise that resolves once the data has been sent 601 | */ 602 | sendToChild(id, data = null) { 603 | return this.__sendToChild(id, EVENT.PARENT_COMMUNICATION, data); 604 | } 605 | 606 | /** 607 | * @function sendToChildren 608 | * @memberof TabTalk 609 | * 610 | * @description 611 | * send data to all children 612 | * 613 | * @param {any} [data=null] the data to send 614 | * @returns {Promise} promise that resolves once the data has been sent to all children 615 | */ 616 | sendToChildren(data = null) { 617 | return this.__sendToChildren(EVENT.PARENT_COMMUNICATION, data); 618 | } 619 | 620 | /** 621 | * @function sendToParent 622 | * @memberof TabTalk 623 | * 624 | * @description 625 | * send data to the parent 626 | * 627 | * @param {any} [data=null] the data to send 628 | * @returns {Promise} promise that resolves once the data has been sent 629 | */ 630 | sendToParent(data = null) { 631 | return this.__sendToParent(EVENT.CHILD_COMMUNICATION, data); 632 | } 633 | } 634 | 635 | export default TabTalk; 636 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @constant {string} ENCRYPTION_KEY 3 | */ 4 | export const ENCRYPTION_KEY = '__TABTALK_ENCRYPTION_KEY__'; 5 | 6 | /** 7 | * @constant {Object} EVENT the events passed via postmessage 8 | */ 9 | export const EVENT = { 10 | CHILD_COMMUNICATION: 'CHILD_COMMUNICATION', 11 | PARENT_COMMUNICATION: 'PARENT_COMMUNICATION', 12 | PING_CHILD: 'PING_CHILD', 13 | PING_PARENT: 'PING_PARENT', 14 | REGISTER: 'REGISTER', 15 | SET_TAB_STATUS: 'SET_TAB_STATUS', 16 | }; 17 | 18 | /** 19 | * @constant {number} PING_INTERVAL the default interval between pings 20 | */ 21 | export const PING_INTERVAL = 5000; 22 | 23 | /** 24 | * @constant {number} PING_CHECKIN_BUFFER the default buffer time to allow tabs not to ping in with 25 | */ 26 | export const PING_CHECKIN_BUFFER = 5000; 27 | 28 | /** 29 | * @constant {number} REGISTRATION_BUFFER the default buffer time to wait for tabs to register 30 | */ 31 | export const REGISTRATION_BUFFER = 10000; 32 | 33 | /** 34 | * @constant {string} SESSION_STORAGE_KEY the base of the key used for sessionStorage of a specific tab 35 | */ 36 | export const SESSION_STORAGE_KEY = '__TABTALK_TAB_METADATA__'; 37 | 38 | /** 39 | * @constant {string} TAB_REFERENCE_KEY the key on the window used for storing freshly-created tabs 40 | */ 41 | export const TAB_REFERENCE_KEY = '__TABTALK_TAB__'; 42 | 43 | /** 44 | * @constant {Object} TAB_STATUS the status of the tab 45 | */ 46 | export const TAB_STATUS = { 47 | CLOSED: 'CLOSED', 48 | OPEN: 'OPEN', 49 | }; 50 | 51 | /** 52 | * @constant {Object} DEFAULT_CONFIG the default config values 53 | */ 54 | export const DEFAULT_CONFIG = { 55 | encryptionKey: ENCRYPTION_KEY, 56 | origin: window.origin || document.domain || '*', 57 | pingCheckinBuffer: PING_CHECKIN_BUFFER, 58 | pingInterval: PING_INTERVAL, 59 | registrationBuffer: REGISTRATION_BUFFER, 60 | removeOnClose: false, 61 | }; 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // tabs 2 | import TabTalk from './TabTalk'; 3 | 4 | /** 5 | * @function createTab 6 | * 7 | * @description 8 | * create a new Tab instance based on the configuration object passed 9 | * 10 | * @param {Object} [config={}] the configuration object 11 | * @returns {Tab} the generated tab 12 | */ 13 | const createTab = (config = {}) => new TabTalk(config, {}); 14 | 15 | export default createTab; 16 | -------------------------------------------------------------------------------- /src/sessionStorage.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | import {call} from 'unchanged'; 3 | 4 | /** 5 | * @function getStorageData 6 | * 7 | * @description 8 | * get the data in sessionStorage for the given key 9 | * 10 | * @param {string} key the key to get the data for 11 | * @returns {any} the stored data 12 | */ 13 | export const getStorageData = (key) => { 14 | const data = call(['getItem'], [key], window.sessionStorage); 15 | 16 | return data ? JSON.parse(data) : null; 17 | }; 18 | 19 | /** 20 | * @function removeStorageData 21 | * 22 | * @description 23 | * remove the given key from sessionStorage 24 | * 25 | * @param {string} key the key to remove from storage 26 | * @returns {void} 27 | */ 28 | export const removeStorageData = (key) => call(['removeItem'], [key], window.sessionStorage); 29 | 30 | /** 31 | * @function setStorageData 32 | * 33 | * @description 34 | * set the data in sessionStorage for the given key 35 | * 36 | * @param {string} key the key to set the data for 37 | * @param {any} data the data to set 38 | * @returns {void} 39 | */ 40 | export const setStorageData = (key, data) => call(['setItem'], [key, JSON.stringify(data)], window.sessionStorage); 41 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // constants 2 | import {SESSION_STORAGE_KEY} from './constants'; 3 | 4 | /** 5 | * @function map 6 | * 7 | * @description 8 | * map the array based on returns from calling fn 9 | * 10 | * @param {function} fn the function to get mapped value from 11 | * @param {Array} array the array to iterate over 12 | * @returns {any} the mapped value 13 | */ 14 | export const map = (fn, array) => { 15 | let mapped = new Array(array.length); 16 | 17 | for (let index = 0; index < array.length; index++) { 18 | mapped[index] = fn(array[index], index, array); 19 | } 20 | 21 | return mapped; 22 | }; 23 | 24 | /** 25 | * @function filter 26 | * 27 | * @description 28 | * filter the array based on truthy returns from calling fn 29 | * 30 | * @param {function} fn the function that filters the array based on calling fn with each iteration 31 | * @param {Array} array the array to filter 32 | * @returns {Array} the filtered array 33 | */ 34 | export const filter = (fn, array) => { 35 | let filtered = [], 36 | result; 37 | 38 | for (let index = 0; index < array.length; index++) { 39 | result = fn(array[index], index, array); 40 | 41 | if (result) { 42 | filtered.push(array[index]); 43 | } 44 | } 45 | 46 | return filtered; 47 | }; 48 | 49 | /** 50 | * @function reduce 51 | * 52 | * @description 53 | * reduce the values in array to a single value based on calling fn with each iteration 54 | * 55 | * @param {function} fn the function that reduces each iteration to a value 56 | * @param {any} initialValue the initial value of the reduction 57 | * @param {Array} array the array to reduce 58 | * @returns {any} the reduced value 59 | */ 60 | export const reduce = (fn, initialValue, array) => { 61 | let value = initialValue; 62 | 63 | for (let index = 0; index < array.length; index++) { 64 | value = fn(value, array[index], index, array); 65 | } 66 | 67 | return value; 68 | }; 69 | 70 | /** 71 | * @function assign 72 | * 73 | * @description 74 | * shallowly merge the sources into the target 75 | * 76 | * @param {Object} target the target object to assign into 77 | * @param {...any} sources the sources to assign into the target 78 | * @returns {Object} the shallowly-merged object 79 | */ 80 | export const assign = (target, ...sources) => 81 | reduce( 82 | (assigned, source) => 83 | source 84 | ? reduce( 85 | (assigned, key) => { 86 | assigned[key] = source[key]; 87 | 88 | return assigned; 89 | }, 90 | assigned, 91 | Object.keys(source) 92 | ) 93 | : assigned, 94 | target, 95 | sources 96 | ); 97 | 98 | /** 99 | * @function find 100 | * 101 | * @description 102 | * find an item in the array based on matching with fn 103 | * 104 | * @param {function} fn the function to get the match with 105 | * @param {Array} array the array to iterate over 106 | * @returns {any} the matching value 107 | */ 108 | export const find = (fn, array) => { 109 | for (let index = 0; index < array.length; index++) { 110 | if (fn(array[index], index, array)) { 111 | return array[index]; 112 | } 113 | } 114 | }; 115 | 116 | /** 117 | * @function findChildTab 118 | * 119 | * @description 120 | * find the child tab in the children 121 | * 122 | * @param {Array} children the children of the tab 123 | * @param {string} id the id of the child to find 124 | * @returns {Tab} the matching child tab 125 | */ 126 | export const findChildTab = (children, id) => find(({id: childId}) => childId === id, children); 127 | 128 | /** 129 | * @function getChildWindowName 130 | * 131 | * @description 132 | * build the name of the child window based on it and its parent 133 | * 134 | * @param {string} childId the child id 135 | * @param {string} parentId the parent id 136 | * @returns {string} the child window name 137 | */ 138 | export const getChildWindowName = (childId, parentId) => `${SESSION_STORAGE_KEY}:CHILD_${childId}_OF_${parentId}`; 139 | 140 | /** 141 | * @function getHasTimedOut 142 | * 143 | * @description 144 | * determine if the child has timed out 145 | * 146 | * @param {Tab} child the child tab 147 | * @param {Object} config the configuration of the parent 148 | * @returns {boolean} has the child timed out 149 | */ 150 | export const getHasTimedOut = (child, config) => 151 | child.lastCheckin 152 | ? child.lastCheckin < Date.now() - config.pingInterval - config.pingCheckinBuffer 153 | : child.created + config.registrationBuffer < Date.now(); 154 | -------------------------------------------------------------------------------- /test/TabTalk.js: -------------------------------------------------------------------------------- 1 | // test 2 | import test from 'ava'; 3 | import { 4 | decrypt, 5 | encrypt 6 | } from 'krip'; 7 | import sinon from 'sinon'; 8 | 9 | // src 10 | import TabTalk from 'src/TabTalk'; 11 | import { 12 | DEFAULT_CONFIG, 13 | EVENT, 14 | SESSION_STORAGE_KEY, 15 | TAB_REFERENCE_KEY, 16 | TAB_STATUS 17 | } from 'src/constants'; 18 | import * as storage from 'src/sessionStorage'; 19 | 20 | import * as utils from 'src/utils'; 21 | 22 | test('if the new tab instance has the correct properties', (t) => { 23 | window.name = ''; 24 | 25 | const addEventListenerStub = sinon.stub(window, 'addEventListener'); 26 | const removeStorageDataStub = sinon.stub(storage, 'removeStorageData'); 27 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 28 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 29 | 30 | const result = new TabTalk({}, {}); 31 | 32 | t.deepEqual(result.__children, []); 33 | t.deepEqual(result.config, DEFAULT_CONFIG); 34 | t.is(typeof result.created, 'number'); 35 | t.regex(result.id, /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/); 36 | t.is(result.lastCheckin, null); 37 | t.is(result.lastParentCheckin, null); 38 | t.is(result.parent, null); 39 | t.is(result.receivePingInterval, null); 40 | t.is(result.ref, window); 41 | t.is(result.sendPingInterval, null); 42 | t.is(result.status, TAB_STATUS.OPEN); 43 | t.is(result.windowName, `${SESSION_STORAGE_KEY}:ROOT`); 44 | 45 | t.true(addEventListenerStub.calledTwice); 46 | 47 | const [beforeUnload, message] = addEventListenerStub.args; 48 | 49 | t.is(beforeUnload[0], 'beforeunload'); 50 | t.is(message[0], 'message'); 51 | 52 | addEventListenerStub.restore(); 53 | 54 | t.true(removeStorageDataStub.calledOnce); 55 | t.true(removeStorageDataStub.calledWith(result.windowName)); 56 | 57 | removeStorageDataStub.restore(); 58 | 59 | t.true(receivePingStub.calledOnce); 60 | 61 | receivePingStub.restore(); 62 | 63 | t.true(sendPingStub.calledOnce); 64 | 65 | sendPingStub.restore(); 66 | }); 67 | 68 | test('if the new tab instance has the correct properties when additional stuff is passed', (t) => { 69 | window.name = 'TAB_TEST'; 70 | 71 | const currentWindow = window; 72 | 73 | window = {...currentWindow}; 74 | 75 | window.opener = currentWindow; 76 | 77 | const addEventListenerStub = sinon.stub(window, 'addEventListener'); 78 | const addEventListenerStubParent = sinon.stub(currentWindow, 'addEventListener'); 79 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 80 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 81 | 82 | const result = new TabTalk({}, {ref: window}); 83 | 84 | t.deepEqual(result.__children, []); 85 | t.deepEqual(result.config, DEFAULT_CONFIG); 86 | t.is(typeof result.created, 'number'); 87 | t.regex(result.id, /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/); 88 | t.is(result.lastCheckin, null); 89 | t.is(result.lastParentCheckin, null); 90 | t.is(result.parent, currentWindow); 91 | t.is(result.receivePingInterval, null); 92 | t.is(result.ref, window); 93 | t.is(result.sendPingInterval, null); 94 | t.is(result.status, TAB_STATUS.OPEN); 95 | t.is(result.windowName, window.name); 96 | 97 | t.true(addEventListenerStub.calledTwice); 98 | 99 | const [beforeUnload, message] = addEventListenerStub.args; 100 | 101 | t.is(beforeUnload[0], 'beforeunload'); 102 | t.is(message[0], 'message'); 103 | t.is(message[1], result.__handleMessage); 104 | 105 | addEventListenerStub.restore(); 106 | 107 | t.true(addEventListenerStubParent.calledOnce); 108 | 109 | const [beforeUnloadParent] = addEventListenerStubParent.args; 110 | 111 | t.is(beforeUnloadParent[0], 'beforeunload'); 112 | 113 | addEventListenerStubParent.restore(); 114 | 115 | t.true(receivePingStub.calledOnce); 116 | 117 | receivePingStub.restore(); 118 | 119 | t.true(sendPingStub.calledOnce); 120 | 121 | sendPingStub.restore(); 122 | 123 | window = currentWindow; 124 | }); 125 | 126 | test('if the children getter returns a shallow clone of the children', (t) => { 127 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 128 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 129 | 130 | const tab = new TabTalk({}, {}); 131 | 132 | tab.__children = [ 133 | { 134 | id: 'foo', 135 | status: 'OPEN', 136 | }, 137 | { 138 | id: 'bar', 139 | status: 'CLOSED', 140 | }, 141 | { 142 | id: 'baz', 143 | status: 'OPEN', 144 | }, 145 | ]; 146 | 147 | const result = tab.children; 148 | 149 | t.not(result, tab.__children); 150 | t.deepEqual(result, tab.__children); 151 | 152 | receivePingStub.restore(); 153 | sendPingStub.restore(); 154 | }); 155 | 156 | test('if the openChildren getter returns a shallow clone of the open children', (t) => { 157 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 158 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 159 | 160 | const tab = new TabTalk({}, {}); 161 | 162 | tab.__children = [ 163 | { 164 | id: 'foo', 165 | status: 'OPEN', 166 | }, 167 | { 168 | id: 'bar', 169 | status: 'CLOSED', 170 | }, 171 | { 172 | id: 'baz', 173 | status: 'OPEN', 174 | }, 175 | ]; 176 | 177 | const result = tab.openChildren; 178 | 179 | t.not(result, tab.__children); 180 | t.deepEqual(result, [tab.__children[0], tab.__children[2]]); 181 | 182 | receivePingStub.restore(); 183 | sendPingStub.restore(); 184 | }); 185 | 186 | test('if the closedChildren getter returns a shallow clone of the closed children', (t) => { 187 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 188 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 189 | 190 | const tab = new TabTalk({}, {}); 191 | 192 | tab.__children = [ 193 | { 194 | id: 'foo', 195 | status: 'OPEN', 196 | }, 197 | { 198 | id: 'bar', 199 | status: 'CLOSED', 200 | }, 201 | { 202 | id: 'baz', 203 | status: 'OPEN', 204 | }, 205 | ]; 206 | 207 | const result = tab.closedChildren; 208 | 209 | t.not(result, tab.__children); 210 | t.deepEqual(result, [tab.__children[1]]); 211 | 212 | receivePingStub.restore(); 213 | sendPingStub.restore(); 214 | }); 215 | 216 | test('if __addChild will add a new child to the internal children', (t) => { 217 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 218 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 219 | 220 | const tab = new TabTalk({}, {}); 221 | 222 | const child = {id: 'foo'}; 223 | 224 | tab.__addChild(child); 225 | 226 | t.deepEqual(tab.__children, [child]); 227 | 228 | receivePingStub.restore(); 229 | sendPingStub.restore(); 230 | }); 231 | 232 | test.serial('if __addEventListeners will add the appropriate event listeners when there is a parent', (t) => { 233 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 234 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 235 | 236 | const addStub = sinon.stub(window, 'addEventListener'); 237 | const setStorageStub = sinon.stub(storage, 'setStorageData'); 238 | const clearStub = sinon.stub(TabTalk.prototype, '__clearPingIntervals'); 239 | const parentStub = sinon.stub(TabTalk.prototype, '__sendToParent'); 240 | 241 | const config = { 242 | onClose: sinon.spy(), 243 | onParentClose: sinon.spy(), 244 | }; 245 | 246 | const currentOpener = window.opener; 247 | 248 | window.opener = {...window}; 249 | 250 | const tab = new TabTalk(config, {}); 251 | 252 | addStub.reset(); 253 | clearStub.reset(); 254 | parentStub.reset(); 255 | 256 | tab.parent = window; 257 | 258 | tab.__addEventListeners(); 259 | 260 | t.true(addStub.calledThrice); 261 | 262 | const [beforeUnload, message, beforeParentUnload] = addStub.args; 263 | 264 | t.is(beforeUnload[0], 'beforeunload'); 265 | 266 | beforeUnload[1](); 267 | 268 | t.true(setStorageStub.calledOnce); 269 | t.true(setStorageStub.calledWith(tab.windowName, {id: tab.id})); 270 | 271 | setStorageStub.restore(); 272 | 273 | t.true(clearStub.calledOnce); 274 | 275 | clearStub.restore(); 276 | 277 | t.true(parentStub.calledOnce); 278 | t.true(parentStub.calledWith(EVENT.SET_TAB_STATUS, TAB_STATUS.CLOSED)); 279 | 280 | parentStub.restore(); 281 | 282 | t.true(config.onClose.calledOnce); 283 | 284 | t.deepEqual(message, ['message', tab.__handleMessage]); 285 | 286 | t.is(beforeParentUnload[0], 'beforeunload'); 287 | 288 | beforeParentUnload[1](); 289 | 290 | t.true(config.onParentClose.calledOnce); 291 | 292 | addStub.restore(); 293 | 294 | receivePingStub.restore(); 295 | sendPingStub.restore(); 296 | 297 | window.opener = currentOpener; 298 | }); 299 | 300 | test.serial('if __addEventListeners will add the appropriate event listeners when there is no parent', (t) => { 301 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 302 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 303 | 304 | const addStub = sinon.stub(window, 'addEventListener'); 305 | const setStorageStub = sinon.stub(storage, 'setStorageData'); 306 | const clearStub = sinon.stub(TabTalk.prototype, '__clearPingIntervals'); 307 | const parentStub = sinon.stub(TabTalk.prototype, '__sendToParent'); 308 | 309 | const config = { 310 | onClose: sinon.spy(), 311 | onParentClose: sinon.spy(), 312 | }; 313 | 314 | const tab = new TabTalk(config, {}); 315 | 316 | addStub.reset(); 317 | clearStub.reset(); 318 | parentStub.reset(); 319 | 320 | tab.__addEventListeners(); 321 | 322 | t.true(addStub.calledTwice); 323 | 324 | const [beforeUnload, message] = addStub.args; 325 | 326 | t.is(beforeUnload[0], 'beforeunload'); 327 | 328 | beforeUnload[1](); 329 | 330 | t.true(setStorageStub.calledOnce); 331 | t.true(setStorageStub.calledWith(tab.windowName, {id: tab.id})); 332 | 333 | setStorageStub.restore(); 334 | 335 | t.true(clearStub.calledOnce); 336 | 337 | clearStub.restore(); 338 | 339 | t.true(parentStub.notCalled); 340 | 341 | parentStub.restore(); 342 | 343 | t.true(config.onClose.calledOnce); 344 | 345 | t.deepEqual(message, ['message', tab.__handleMessage]); 346 | 347 | t.true(config.onParentClose.notCalled); 348 | 349 | addStub.restore(); 350 | 351 | receivePingStub.restore(); 352 | sendPingStub.restore(); 353 | }); 354 | 355 | test('if __clearPingIntervals will clear the intervals for both send and receive', (t) => { 356 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 357 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 358 | 359 | const clearStub = sinon.stub(global, 'clearInterval'); 360 | 361 | const config = {}; 362 | 363 | const tab = new TabTalk(config, {}); 364 | 365 | clearStub.reset(); 366 | 367 | tab.receivePingInterval = 123; 368 | tab.sendPingInterval = 234; 369 | 370 | tab.__clearPingIntervals(); 371 | 372 | t.true(clearStub.calledTwice); 373 | t.deepEqual(clearStub.args, [[tab.receivePingInterval], [tab.sendPingInterval]]); 374 | 375 | clearStub.restore(); 376 | 377 | receivePingStub.restore(); 378 | sendPingStub.restore(); 379 | }); 380 | 381 | test('if __handleOnChildCommunicationMessage will call onChildCommunication if there is a child', (t) => { 382 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 383 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 384 | 385 | const config = { 386 | onChildCommunication: sinon.spy(), 387 | }; 388 | 389 | const tab = new TabTalk(config, {}); 390 | 391 | const payload = { 392 | child: 'child', 393 | data: 'data', 394 | }; 395 | 396 | tab.__handleOnChildCommunicationMessage(payload); 397 | 398 | t.true(config.onChildCommunication.calledOnce); 399 | 400 | const [data, time] = config.onChildCommunication.args[0]; 401 | 402 | t.is(data, payload.data); 403 | t.is(typeof time, 'number'); 404 | 405 | receivePingStub.restore(); 406 | sendPingStub.restore(); 407 | }); 408 | 409 | test('if __handleOnChildCommunicationMessage will not call onChildCommunication if there is no child', (t) => { 410 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 411 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 412 | 413 | const config = { 414 | onChildCommunication: sinon.spy(), 415 | }; 416 | 417 | const tab = new TabTalk(config, {}); 418 | 419 | const payload = { 420 | child: null, 421 | data: 'data', 422 | }; 423 | 424 | tab.__handleOnChildCommunicationMessage(payload); 425 | 426 | t.true(config.onChildCommunication.notCalled); 427 | 428 | receivePingStub.restore(); 429 | sendPingStub.restore(); 430 | }); 431 | 432 | test('if __handleOnParentCommunicationMessage will call onParentCommunication if the childId matches', (t) => { 433 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 434 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 435 | 436 | const config = { 437 | onParentCommunication: sinon.spy(), 438 | }; 439 | 440 | const tab = new TabTalk(config, {}); 441 | 442 | const payload = { 443 | childId: tab.id, 444 | data: 'data', 445 | }; 446 | 447 | tab.__handleOnParentCommunicationMessage(payload); 448 | 449 | t.true(config.onParentCommunication.calledOnce); 450 | 451 | const [data, time] = config.onParentCommunication.args[0]; 452 | 453 | t.is(data, payload.data); 454 | t.is(typeof time, 'number'); 455 | 456 | receivePingStub.restore(); 457 | sendPingStub.restore(); 458 | }); 459 | 460 | test('if __handleOnParentCommunicationMessage will not call onParentCommunication if the childId does not match', (t) => { 461 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 462 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 463 | 464 | const config = { 465 | onParentCommunication: sinon.spy(), 466 | }; 467 | 468 | const tab = new TabTalk(config, {}); 469 | 470 | const payload = { 471 | childId: 'blah', 472 | data: 'data', 473 | }; 474 | 475 | tab.__handleOnParentCommunicationMessage(payload); 476 | 477 | t.true(config.onParentCommunication.notCalled); 478 | 479 | receivePingStub.restore(); 480 | sendPingStub.restore(); 481 | }); 482 | 483 | test('if __handlePingChildMessage will set the lastParentCheckin value if the childId matches', (t) => { 484 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 485 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 486 | 487 | const config = {}; 488 | 489 | const tab = new TabTalk(config, {}); 490 | 491 | const payload = { 492 | childId: tab.id, 493 | data: 12345, 494 | }; 495 | 496 | tab.__handlePingChildMessage(payload); 497 | 498 | t.is(tab.lastParentCheckin, payload.data); 499 | 500 | receivePingStub.restore(); 501 | sendPingStub.restore(); 502 | }); 503 | 504 | test('if __handlePingChildMessage will not call onParentCommunication if the childId does not match', (t) => { 505 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 506 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 507 | 508 | const config = {}; 509 | 510 | const tab = new TabTalk(config, {}); 511 | 512 | const payload = { 513 | childId: 'blah', 514 | data: 12345, 515 | }; 516 | 517 | tab.__handlePingChildMessage(payload); 518 | 519 | t.is(tab.lastParentCheckin, null); 520 | 521 | receivePingStub.restore(); 522 | sendPingStub.restore(); 523 | }); 524 | 525 | test('if __handlePingParentOrRegisterMessage will set the lastCheckin of the existing child', (t) => { 526 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 527 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 528 | 529 | const config = { 530 | onChildRegister: sinon.spy(), 531 | }; 532 | 533 | const tab = new TabTalk(config, {}); 534 | const child = new TabTalk(config, { 535 | ref: tab.ref, 536 | windowName: 'child', 537 | }); 538 | 539 | child.lastCheckin = 12345; 540 | 541 | const payload = { 542 | child, 543 | id: child.id, 544 | lastCheckin: Date.now(), 545 | source: child.ref, 546 | }; 547 | 548 | const result = tab.__handlePingParentOrRegisterMessage(payload); 549 | 550 | t.is(child.lastCheckin, payload.lastCheckin); 551 | t.is(result, child); 552 | 553 | t.true(config.onChildRegister.notCalled); 554 | 555 | receivePingStub.restore(); 556 | sendPingStub.restore(); 557 | }); 558 | 559 | test('if __handlePingParentOrRegisterMessage will call onRegister if the child has never checked in', (t) => { 560 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 561 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 562 | 563 | const config = { 564 | onChildRegister: sinon.spy(), 565 | }; 566 | 567 | const tab = new TabTalk(config, {}); 568 | const child = new TabTalk(config, { 569 | ref: tab.ref, 570 | windowName: 'child', 571 | }); 572 | 573 | const payload = { 574 | child, 575 | id: child.id, 576 | lastCheckin: Date.now(), 577 | source: child.ref, 578 | }; 579 | 580 | const result = tab.__handlePingParentOrRegisterMessage(payload); 581 | 582 | t.is(child.lastCheckin, payload.lastCheckin); 583 | t.is(result, child); 584 | 585 | t.true(config.onChildRegister.calledOnce); 586 | t.true(config.onChildRegister.calledWith(child)); 587 | 588 | receivePingStub.restore(); 589 | sendPingStub.restore(); 590 | }); 591 | 592 | test('if __handlePingParentOrRegisterMessage will create and add a child before setting the lastCheckin of the new child', (t) => { 593 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 594 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 595 | 596 | const addStub = sinon.stub(TabTalk.prototype, '__addChild'); 597 | 598 | const config = { 599 | onChildRegister: sinon.spy(), 600 | }; 601 | 602 | const tab = new TabTalk(config, {}); 603 | 604 | const payload = { 605 | id: 'childId', 606 | lastCheckin: Date.now(), 607 | source: tab.ref, 608 | }; 609 | 610 | const child = tab.__handlePingParentOrRegisterMessage(payload); 611 | 612 | t.true(addStub.calledOnce); 613 | t.true(addStub.calledWith(child)); 614 | 615 | addStub.restore(); 616 | 617 | t.is(child.lastCheckin, payload.lastCheckin); 618 | 619 | t.true(config.onChildRegister.calledOnce); 620 | t.true(config.onChildRegister.calledWith(child)); 621 | 622 | receivePingStub.restore(); 623 | sendPingStub.restore(); 624 | }); 625 | 626 | test('if __handleSetStatusMessage will set the status of the child', (t) => { 627 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 628 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 629 | 630 | const removeStub = sinon.stub(TabTalk.prototype, '__removeChild'); 631 | 632 | const config = { 633 | onChildClose: sinon.spy(), 634 | }; 635 | 636 | const tab = new TabTalk(config, {}); 637 | const child = new TabTalk(config, {windowName: 'child'}); 638 | 639 | tab.__addChild(child); 640 | 641 | const payload = { 642 | child, 643 | status: TAB_STATUS.CLOSED, 644 | }; 645 | 646 | tab.__handleSetStatusMessage(payload); 647 | 648 | t.is(child.status, payload.status); 649 | 650 | t.true(config.onChildClose.calledOnce); 651 | t.true(config.onChildClose.calledWith(child)); 652 | 653 | t.true(removeStub.notCalled); 654 | 655 | removeStub.restore(); 656 | 657 | receivePingStub.restore(); 658 | sendPingStub.restore(); 659 | }); 660 | 661 | test('if __handleSetStatusMessage will remove the child if closed and requested in the config', (t) => { 662 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 663 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 664 | 665 | const removeStub = sinon.stub(TabTalk.prototype, '__removeChild'); 666 | 667 | const config = { 668 | onChildClose: sinon.spy(), 669 | removeOnClosed: true, 670 | }; 671 | 672 | const tab = new TabTalk(config, {}); 673 | const child = new TabTalk(config, {windowName: 'child'}); 674 | 675 | tab.__addChild(child); 676 | 677 | const payload = { 678 | child, 679 | status: TAB_STATUS.CLOSED, 680 | }; 681 | 682 | tab.__handleSetStatusMessage(payload); 683 | 684 | t.is(child.status, payload.status); 685 | 686 | t.true(config.onChildClose.calledOnce); 687 | t.true(config.onChildClose.calledWith(child)); 688 | 689 | t.true(removeStub.calledOnce); 690 | t.true(removeStub.calledWith(child)); 691 | 692 | removeStub.restore(); 693 | 694 | receivePingStub.restore(); 695 | sendPingStub.restore(); 696 | }); 697 | 698 | test('if __handleSetStatusMessage will not call close methods when the status is set to open', (t) => { 699 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 700 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 701 | 702 | const removeStub = sinon.stub(TabTalk.prototype, '__removeChild'); 703 | 704 | const config = { 705 | onChildClose: sinon.spy(), 706 | removeOnClosed: true, 707 | }; 708 | 709 | const tab = new TabTalk(config, {}); 710 | const child = new TabTalk(config, {windowName: 'child'}); 711 | 712 | child.status = TAB_STATUS.CLOSED; 713 | 714 | tab.__addChild(child); 715 | 716 | const payload = { 717 | child, 718 | status: TAB_STATUS.OPEN, 719 | }; 720 | 721 | tab.__handleSetStatusMessage(payload); 722 | 723 | t.is(child.status, payload.status); 724 | 725 | t.true(config.onChildClose.notCalled); 726 | 727 | t.true(removeStub.notCalled); 728 | 729 | removeStub.restore(); 730 | 731 | receivePingStub.restore(); 732 | sendPingStub.restore(); 733 | }); 734 | 735 | test('if __handleSetStatusMessage will do nothing if no child is passed', (t) => { 736 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 737 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 738 | 739 | const removeStub = sinon.stub(TabTalk.prototype, '__removeChild'); 740 | 741 | const config = { 742 | onChildClose: sinon.spy(), 743 | removeOnClosed: true, 744 | }; 745 | 746 | const tab = new TabTalk(config, {}); 747 | const child = new TabTalk(config, {windowName: 'child'}); 748 | 749 | tab.__addChild(child); 750 | 751 | const payload = { 752 | child: null, 753 | status: TAB_STATUS.CLOSED, 754 | }; 755 | 756 | tab.__handleSetStatusMessage(payload); 757 | 758 | t.true(config.onChildClose.notCalled); 759 | 760 | t.true(removeStub.notCalled); 761 | 762 | removeStub.restore(); 763 | 764 | receivePingStub.restore(); 765 | sendPingStub.restore(); 766 | }); 767 | 768 | test.serial('if __handleMessage will call the correct handler when the PING_CHILD event is sent', async (t) => { 769 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 770 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 771 | 772 | const pingChildStub = sinon.stub(TabTalk.prototype, '__handlePingChildMessage'); 773 | const pingParentStub = sinon.stub(TabTalk.prototype, '__handlePingParentOrRegisterMessage'); 774 | const setStatusStub = sinon.stub(TabTalk.prototype, '__handleSetStatusMessage'); 775 | const childCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnChildCommunicationMessage'); 776 | const parentCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnParentCommunicationMessage'); 777 | 778 | const config = {}; 779 | 780 | const tab = new TabTalk(config, {}); 781 | 782 | console.log(tab.config.encryptionKey); 783 | 784 | const rawData = 'data'; 785 | const data = await encrypt(rawData, tab.config.encryptionKey); 786 | 787 | const payload = { 788 | data: JSON.stringify({ 789 | data, 790 | event: EVENT.PING_CHILD, 791 | }), 792 | origin: tab.config.origin, 793 | source: tab.ref, 794 | }; 795 | 796 | await tab.__handleMessage(payload); 797 | 798 | t.true(pingChildStub.calledOnce); 799 | t.true(pingChildStub.calledWith(rawData)); 800 | 801 | t.true(pingParentStub.notCalled); 802 | 803 | t.true(setStatusStub.notCalled); 804 | 805 | t.true(childCommunicationStub.notCalled); 806 | 807 | t.true(parentCommunicationStub.notCalled); 808 | 809 | pingChildStub.restore(); 810 | pingParentStub.restore(); 811 | setStatusStub.restore(); 812 | childCommunicationStub.restore(); 813 | parentCommunicationStub.restore(); 814 | 815 | receivePingStub.restore(); 816 | sendPingStub.restore(); 817 | }); 818 | 819 | test.serial('if __handleMessage will call the correct handler when the PING_PARENT event is sent', async (t) => { 820 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 821 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 822 | 823 | const pingChildStub = sinon.stub(TabTalk.prototype, '__handlePingChildMessage'); 824 | const pingParentStub = sinon.stub(TabTalk.prototype, '__handlePingParentOrRegisterMessage'); 825 | const setStatusStub = sinon.stub(TabTalk.prototype, '__handleSetStatusMessage'); 826 | const childCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnChildCommunicationMessage'); 827 | const parentCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnParentCommunicationMessage'); 828 | 829 | const config = {}; 830 | 831 | const tab = new TabTalk(config, {}); 832 | const child = new TabTalk(config, {windowName: 'child'}); 833 | 834 | tab.__addChild(child); 835 | 836 | const rawData = 'data'; 837 | const data = await encrypt(rawData, tab.config.encryptionKey); 838 | 839 | const payload = { 840 | data: JSON.stringify({ 841 | data, 842 | event: EVENT.PING_PARENT, 843 | id: child.id, 844 | }), 845 | origin: tab.config.origin, 846 | source: tab.ref, 847 | }; 848 | 849 | await tab.__handleMessage(payload); 850 | 851 | t.true(pingChildStub.notCalled); 852 | 853 | t.true(pingParentStub.calledOnce); 854 | t.true(pingParentStub.calledWith({ 855 | child, 856 | id: JSON.parse(payload.data).id, 857 | lastCheckin: rawData, 858 | source: payload.source, 859 | })); 860 | 861 | t.true(setStatusStub.notCalled); 862 | 863 | t.true(childCommunicationStub.notCalled); 864 | 865 | t.true(parentCommunicationStub.notCalled); 866 | 867 | pingChildStub.restore(); 868 | pingParentStub.restore(); 869 | setStatusStub.restore(); 870 | childCommunicationStub.restore(); 871 | parentCommunicationStub.restore(); 872 | 873 | receivePingStub.restore(); 874 | sendPingStub.restore(); 875 | }); 876 | 877 | test.serial('if __handleMessage will call the correct handler when the REGISTER event is sent', async (t) => { 878 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 879 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 880 | 881 | const pingChildStub = sinon.stub(TabTalk.prototype, '__handlePingChildMessage'); 882 | const pingParentStub = sinon.stub(TabTalk.prototype, '__handlePingParentOrRegisterMessage'); 883 | const setStatusStub = sinon.stub(TabTalk.prototype, '__handleSetStatusMessage'); 884 | const childCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnChildCommunicationMessage'); 885 | const parentCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnParentCommunicationMessage'); 886 | 887 | const config = {}; 888 | 889 | const tab = new TabTalk(config, {}); 890 | const child = new TabTalk(config, {windowName: 'child'}); 891 | 892 | tab.__addChild(child); 893 | 894 | const rawData = 'data'; 895 | const data = await encrypt(rawData, tab.config.encryptionKey); 896 | 897 | const payload = { 898 | data: JSON.stringify({ 899 | data, 900 | event: EVENT.REGISTER, 901 | id: child.id, 902 | }), 903 | origin: tab.config.origin, 904 | source: tab.ref, 905 | }; 906 | 907 | await tab.__handleMessage(payload); 908 | 909 | t.true(pingChildStub.notCalled); 910 | 911 | t.true(pingParentStub.calledOnce); 912 | t.true(pingParentStub.calledWith({ 913 | child, 914 | id: JSON.parse(payload.data).id, 915 | lastCheckin: rawData, 916 | source: payload.source, 917 | })); 918 | 919 | t.true(setStatusStub.notCalled); 920 | 921 | t.true(childCommunicationStub.notCalled); 922 | 923 | t.true(parentCommunicationStub.notCalled); 924 | 925 | pingChildStub.restore(); 926 | pingParentStub.restore(); 927 | setStatusStub.restore(); 928 | childCommunicationStub.restore(); 929 | parentCommunicationStub.restore(); 930 | 931 | receivePingStub.restore(); 932 | sendPingStub.restore(); 933 | }); 934 | 935 | test.serial('if __handleMessage will call the correct handler when the SET_STATUS event is sent', async (t) => { 936 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 937 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 938 | 939 | const pingChildStub = sinon.stub(TabTalk.prototype, '__handlePingChildMessage'); 940 | const pingParentStub = sinon.stub(TabTalk.prototype, '__handlePingParentOrRegisterMessage'); 941 | const setStatusStub = sinon.stub(TabTalk.prototype, '__handleSetStatusMessage'); 942 | const childCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnChildCommunicationMessage'); 943 | const parentCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnParentCommunicationMessage'); 944 | 945 | const config = {}; 946 | 947 | const tab = new TabTalk(config, {}); 948 | const child = new TabTalk(config, {windowName: 'child'}); 949 | 950 | tab.__addChild(child); 951 | 952 | const rawData = 'data'; 953 | const data = await encrypt(rawData, tab.config.encryptionKey); 954 | 955 | const payload = { 956 | data: JSON.stringify({ 957 | data, 958 | event: EVENT.SET_TAB_STATUS, 959 | id: child.id, 960 | }), 961 | origin: tab.config.origin, 962 | source: tab.ref, 963 | }; 964 | 965 | await tab.__handleMessage(payload); 966 | 967 | t.true(pingChildStub.notCalled); 968 | 969 | t.true(pingParentStub.notCalled); 970 | 971 | t.true(setStatusStub.calledOnce); 972 | t.true(setStatusStub.calledWith({ 973 | child, 974 | status: rawData, 975 | })); 976 | 977 | t.true(childCommunicationStub.notCalled); 978 | 979 | t.true(parentCommunicationStub.notCalled); 980 | 981 | pingChildStub.restore(); 982 | pingParentStub.restore(); 983 | setStatusStub.restore(); 984 | childCommunicationStub.restore(); 985 | parentCommunicationStub.restore(); 986 | 987 | receivePingStub.restore(); 988 | sendPingStub.restore(); 989 | }); 990 | 991 | test.serial('if __handleMessage will call the correct handler when the CHILD_COMMUNICATION event is sent', async (t) => { 992 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 993 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 994 | 995 | const pingChildStub = sinon.stub(TabTalk.prototype, '__handlePingChildMessage'); 996 | const pingParentStub = sinon.stub(TabTalk.prototype, '__handlePingParentOrRegisterMessage'); 997 | const setStatusStub = sinon.stub(TabTalk.prototype, '__handleSetStatusMessage'); 998 | const childCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnChildCommunicationMessage'); 999 | const parentCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnParentCommunicationMessage'); 1000 | 1001 | const config = {}; 1002 | 1003 | const tab = new TabTalk(config, {}); 1004 | const child = new TabTalk(config, {windowName: 'child'}); 1005 | 1006 | tab.__addChild(child); 1007 | 1008 | const rawData = 'data'; 1009 | const data = await encrypt(rawData, tab.config.encryptionKey); 1010 | 1011 | const payload = { 1012 | data: JSON.stringify({ 1013 | data, 1014 | event: EVENT.CHILD_COMMUNICATION, 1015 | id: child.id, 1016 | }), 1017 | origin: tab.config.origin, 1018 | source: tab.ref, 1019 | }; 1020 | 1021 | await tab.__handleMessage(payload); 1022 | 1023 | t.true(pingChildStub.notCalled); 1024 | 1025 | t.true(pingParentStub.notCalled); 1026 | 1027 | t.true(setStatusStub.notCalled); 1028 | 1029 | t.true(childCommunicationStub.calledOnce); 1030 | t.true(childCommunicationStub.calledWith({ 1031 | child, 1032 | data: rawData, 1033 | })); 1034 | 1035 | t.true(parentCommunicationStub.notCalled); 1036 | 1037 | pingChildStub.restore(); 1038 | pingParentStub.restore(); 1039 | setStatusStub.restore(); 1040 | childCommunicationStub.restore(); 1041 | parentCommunicationStub.restore(); 1042 | 1043 | receivePingStub.restore(); 1044 | sendPingStub.restore(); 1045 | }); 1046 | 1047 | test.serial('if __handleMessage will call the correct handler when the PARENT_COMMUNICATION event is sent', async (t) => { 1048 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1049 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1050 | 1051 | const pingChildStub = sinon.stub(TabTalk.prototype, '__handlePingChildMessage'); 1052 | const pingParentStub = sinon.stub(TabTalk.prototype, '__handlePingParentOrRegisterMessage'); 1053 | const setStatusStub = sinon.stub(TabTalk.prototype, '__handleSetStatusMessage'); 1054 | const childCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnChildCommunicationMessage'); 1055 | const parentCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnParentCommunicationMessage'); 1056 | 1057 | const config = {}; 1058 | 1059 | const tab = new TabTalk(config, {}); 1060 | const child = new TabTalk(config, {windowName: 'child'}); 1061 | 1062 | tab.__addChild(child); 1063 | 1064 | const rawData = 'data'; 1065 | const data = await encrypt(rawData, tab.config.encryptionKey); 1066 | 1067 | const payload = { 1068 | data: JSON.stringify({ 1069 | data, 1070 | event: EVENT.PARENT_COMMUNICATION, 1071 | id: child.id, 1072 | }), 1073 | origin: tab.config.origin, 1074 | source: tab.ref, 1075 | }; 1076 | 1077 | await tab.__handleMessage(payload); 1078 | 1079 | t.true(pingChildStub.notCalled); 1080 | 1081 | t.true(pingParentStub.notCalled); 1082 | 1083 | t.true(setStatusStub.notCalled); 1084 | 1085 | t.true(childCommunicationStub.notCalled); 1086 | 1087 | t.true(parentCommunicationStub.calledOnce); 1088 | t.true(parentCommunicationStub.calledWith(rawData)); 1089 | 1090 | pingChildStub.restore(); 1091 | pingParentStub.restore(); 1092 | setStatusStub.restore(); 1093 | childCommunicationStub.restore(); 1094 | parentCommunicationStub.restore(); 1095 | 1096 | receivePingStub.restore(); 1097 | sendPingStub.restore(); 1098 | }); 1099 | 1100 | test.serial('if __handleMessage will do nothing if the origin does not match', async (t) => { 1101 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1102 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1103 | 1104 | const pingChildStub = sinon.stub(TabTalk.prototype, '__handlePingChildMessage'); 1105 | const pingParentStub = sinon.stub(TabTalk.prototype, '__handlePingParentOrRegisterMessage'); 1106 | const setStatusStub = sinon.stub(TabTalk.prototype, '__handleSetStatusMessage'); 1107 | const childCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnChildCommunicationMessage'); 1108 | const parentCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnParentCommunicationMessage'); 1109 | 1110 | const config = {}; 1111 | 1112 | const tab = new TabTalk(config, {}); 1113 | const child = new TabTalk(config, {windowName: 'child'}); 1114 | 1115 | tab.__addChild(child); 1116 | 1117 | const payload = { 1118 | data: 'data', 1119 | origin: 'maliciousOrigin', 1120 | source: tab.ref, 1121 | }; 1122 | 1123 | await tab.__handleMessage(payload); 1124 | 1125 | t.true(pingChildStub.notCalled); 1126 | 1127 | t.true(pingParentStub.notCalled); 1128 | 1129 | t.true(setStatusStub.notCalled); 1130 | 1131 | t.true(childCommunicationStub.notCalled); 1132 | 1133 | t.true(parentCommunicationStub.notCalled); 1134 | 1135 | pingChildStub.restore(); 1136 | pingParentStub.restore(); 1137 | setStatusStub.restore(); 1138 | childCommunicationStub.restore(); 1139 | parentCommunicationStub.restore(); 1140 | 1141 | receivePingStub.restore(); 1142 | sendPingStub.restore(); 1143 | }); 1144 | 1145 | test.serial('if __handleMessage will do nothing if the event is not in the event data', async (t) => { 1146 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1147 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1148 | 1149 | const pingChildStub = sinon.stub(TabTalk.prototype, '__handlePingChildMessage'); 1150 | const pingParentStub = sinon.stub(TabTalk.prototype, '__handlePingParentOrRegisterMessage'); 1151 | const setStatusStub = sinon.stub(TabTalk.prototype, '__handleSetStatusMessage'); 1152 | const childCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnChildCommunicationMessage'); 1153 | const parentCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnParentCommunicationMessage'); 1154 | 1155 | const config = {}; 1156 | 1157 | const tab = new TabTalk(config, {}); 1158 | const child = new TabTalk(config, {windowName: 'child'}); 1159 | 1160 | tab.__addChild(child); 1161 | 1162 | const payload = { 1163 | data: 'data', 1164 | origin: tab.config.orgin, 1165 | source: tab.ref, 1166 | }; 1167 | 1168 | await tab.__handleMessage(payload); 1169 | 1170 | t.true(pingChildStub.notCalled); 1171 | 1172 | t.true(pingParentStub.notCalled); 1173 | 1174 | t.true(setStatusStub.notCalled); 1175 | 1176 | t.true(childCommunicationStub.notCalled); 1177 | 1178 | t.true(parentCommunicationStub.notCalled); 1179 | 1180 | pingChildStub.restore(); 1181 | pingParentStub.restore(); 1182 | setStatusStub.restore(); 1183 | childCommunicationStub.restore(); 1184 | parentCommunicationStub.restore(); 1185 | 1186 | receivePingStub.restore(); 1187 | sendPingStub.restore(); 1188 | }); 1189 | 1190 | test.serial('if __handleMessage will do nothing if the event is not valid', async (t) => { 1191 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1192 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1193 | 1194 | const pingChildStub = sinon.stub(TabTalk.prototype, '__handlePingChildMessage'); 1195 | const pingParentStub = sinon.stub(TabTalk.prototype, '__handlePingParentOrRegisterMessage'); 1196 | const setStatusStub = sinon.stub(TabTalk.prototype, '__handleSetStatusMessage'); 1197 | const childCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnChildCommunicationMessage'); 1198 | const parentCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnParentCommunicationMessage'); 1199 | 1200 | const config = {}; 1201 | 1202 | const tab = new TabTalk(config, {}); 1203 | const child = new TabTalk(config, {windowName: 'child'}); 1204 | 1205 | tab.__addChild(child); 1206 | 1207 | const payload = { 1208 | data: JSON.stringify({ 1209 | event: 'maliciousEvent', 1210 | }), 1211 | origin: tab.config.orgin, 1212 | source: tab.ref, 1213 | }; 1214 | 1215 | await tab.__handleMessage(payload); 1216 | 1217 | t.true(pingChildStub.notCalled); 1218 | 1219 | t.true(pingParentStub.notCalled); 1220 | 1221 | t.true(setStatusStub.notCalled); 1222 | 1223 | t.true(childCommunicationStub.notCalled); 1224 | 1225 | t.true(parentCommunicationStub.notCalled); 1226 | 1227 | pingChildStub.restore(); 1228 | pingParentStub.restore(); 1229 | setStatusStub.restore(); 1230 | childCommunicationStub.restore(); 1231 | parentCommunicationStub.restore(); 1232 | 1233 | receivePingStub.restore(); 1234 | sendPingStub.restore(); 1235 | }); 1236 | 1237 | test.serial('if __handleMessage handle the decryption rejection by logging in console', async (t) => { 1238 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1239 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1240 | 1241 | const pingChildStub = sinon.stub(TabTalk.prototype, '__handlePingChildMessage'); 1242 | const pingParentStub = sinon.stub(TabTalk.prototype, '__handlePingParentOrRegisterMessage'); 1243 | const setStatusStub = sinon.stub(TabTalk.prototype, '__handleSetStatusMessage'); 1244 | const childCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnChildCommunicationMessage'); 1245 | const parentCommunicationStub = sinon.stub(TabTalk.prototype, '__handleOnParentCommunicationMessage'); 1246 | 1247 | const config = {}; 1248 | 1249 | const tab = new TabTalk(config, {}); 1250 | 1251 | const payload = { 1252 | data: JSON.stringify({ 1253 | data: 'data', 1254 | event: EVENT.PARENT_COMMUNICATION, 1255 | }), 1256 | origin: tab.config.origin, 1257 | source: tab.ref, 1258 | }; 1259 | 1260 | const consoleStub = sinon.stub(console, 'error'); 1261 | 1262 | await tab.__handleMessage(payload); 1263 | 1264 | t.true(pingChildStub.notCalled); 1265 | 1266 | t.true(pingParentStub.notCalled); 1267 | 1268 | t.true(setStatusStub.notCalled); 1269 | 1270 | t.true(childCommunicationStub.notCalled); 1271 | 1272 | t.true(parentCommunicationStub.notCalled); 1273 | 1274 | pingChildStub.restore(); 1275 | pingParentStub.restore(); 1276 | setStatusStub.restore(); 1277 | childCommunicationStub.restore(); 1278 | parentCommunicationStub.restore(); 1279 | 1280 | t.true(consoleStub.calledOnce); 1281 | 1282 | consoleStub.restore(); 1283 | 1284 | receivePingStub.restore(); 1285 | sendPingStub.restore(); 1286 | }); 1287 | 1288 | test('if __register will call onRegister in the config', (t) => { 1289 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1290 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1291 | 1292 | const config = { 1293 | onRegister: sinon.spy(), 1294 | }; 1295 | 1296 | const tab = new TabTalk(config, {}); 1297 | 1298 | config.onRegister.resetHistory(); 1299 | 1300 | tab.__register(); 1301 | 1302 | t.true(config.onRegister.calledOnce); 1303 | t.true(config.onRegister.calledWith(tab)); 1304 | 1305 | receivePingStub.restore(); 1306 | sendPingStub.restore(); 1307 | }); 1308 | 1309 | test('if __register will call __sendToParent if there is a parent', (t) => { 1310 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1311 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1312 | const sendStub = sinon.stub(TabTalk.prototype, '__sendToParent'); 1313 | 1314 | const config = { 1315 | onRegister: sinon.spy(), 1316 | }; 1317 | 1318 | const tab = new TabTalk(config, {}); 1319 | 1320 | tab.parent = 'parent'; 1321 | 1322 | sendStub.reset(); 1323 | config.onRegister.resetHistory(); 1324 | 1325 | tab.__register(); 1326 | 1327 | t.true(sendStub.calledTwice); 1328 | 1329 | const [[registerEvent, registerTime], [statusEvent, status]] = sendStub.args; 1330 | 1331 | t.is(registerEvent, EVENT.REGISTER); 1332 | t.regex(`${registerTime}`, /[0-9]/); 1333 | 1334 | t.is(statusEvent, EVENT.SET_TAB_STATUS); 1335 | t.is(status, TAB_STATUS.OPEN); 1336 | 1337 | t.true(config.onRegister.calledOnce); 1338 | t.true(config.onRegister.calledWith(tab)); 1339 | 1340 | receivePingStub.restore(); 1341 | sendPingStub.restore(); 1342 | sendStub.restore(); 1343 | }); 1344 | 1345 | test('if __removeChild will remove the child from the internal children', (t) => { 1346 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1347 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1348 | 1349 | const config = {}; 1350 | 1351 | const tab = new TabTalk(config, {}); 1352 | 1353 | const child = {some: 'child'}; 1354 | 1355 | tab.__children.push(child); 1356 | 1357 | t.deepEqual(tab.__children, [child]); 1358 | 1359 | tab.__removeChild(child); 1360 | 1361 | t.deepEqual(tab.__children, []); 1362 | 1363 | receivePingStub.restore(); 1364 | sendPingStub.restore(); 1365 | }); 1366 | 1367 | test.serial('if __sendToChild will send the encrypted message to the child via postMessage', async (t) => { 1368 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1369 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1370 | 1371 | const config = {}; 1372 | 1373 | const tab = new TabTalk(config, {}); 1374 | 1375 | tab.__children = [ 1376 | { 1377 | id: 'foo', 1378 | ref: { 1379 | postMessage: sinon.spy(), 1380 | }, 1381 | status: TAB_STATUS.OPEN, 1382 | }, 1383 | { 1384 | id: 'bar', 1385 | status: TAB_STATUS.CLOSED, 1386 | }, 1387 | ]; 1388 | 1389 | const id = tab.__children[0].id; 1390 | const event = EVENT.PING_CHILD; 1391 | const data = 'data'; 1392 | 1393 | await tab.__sendToChild(id, event, data); 1394 | 1395 | t.true(tab.__children[0].ref.postMessage.calledOnce); 1396 | 1397 | const [message, origin] = tab.__children[0].ref.postMessage.args[0]; 1398 | 1399 | const parsedMessage = JSON.parse(message); 1400 | 1401 | const decryptedData = await decrypt(parsedMessage.data, tab.config.encryptionKey); 1402 | 1403 | t.deepEqual(decryptedData, { 1404 | childId: id, 1405 | data, 1406 | }); 1407 | t.is(origin, tab.config.origin); 1408 | 1409 | receivePingStub.restore(); 1410 | sendPingStub.restore(); 1411 | }); 1412 | 1413 | test.serial('if __sendToChild will send the encrypted message to the child via postMessage with defaults', async (t) => { 1414 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1415 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1416 | 1417 | const config = {}; 1418 | 1419 | const tab = new TabTalk(config, {}); 1420 | 1421 | tab.__children = [ 1422 | { 1423 | id: 'foo', 1424 | ref: { 1425 | postMessage: sinon.spy(), 1426 | }, 1427 | status: TAB_STATUS.OPEN, 1428 | }, 1429 | { 1430 | id: 'bar', 1431 | status: TAB_STATUS.CLOSED, 1432 | }, 1433 | ]; 1434 | 1435 | const id = tab.__children[0].id; 1436 | const event = EVENT.PING_CHILD; 1437 | 1438 | await tab.__sendToChild(id, event); 1439 | 1440 | t.true(tab.__children[0].ref.postMessage.calledOnce); 1441 | 1442 | const [message, origin] = tab.__children[0].ref.postMessage.args[0]; 1443 | 1444 | const parsedMessage = JSON.parse(message); 1445 | 1446 | const decryptedData = await decrypt(parsedMessage.data, tab.config.encryptionKey); 1447 | 1448 | t.deepEqual(decryptedData, { 1449 | childId: id, 1450 | data: null, 1451 | }); 1452 | t.is(origin, tab.config.origin); 1453 | 1454 | receivePingStub.restore(); 1455 | sendPingStub.restore(); 1456 | }); 1457 | 1458 | test.serial('if __sendToChild will reject if no child is found', async (t) => { 1459 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1460 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1461 | 1462 | const config = {}; 1463 | 1464 | const tab = new TabTalk(config, {}); 1465 | 1466 | tab.__children = []; 1467 | 1468 | const id = 'id'; 1469 | const event = EVENT.PING_CHILD; 1470 | const data = 'data'; 1471 | 1472 | try { 1473 | await tab.__sendToChild(id, event, data); 1474 | 1475 | t.is(error.message, 'Child could not be found.'); 1476 | } catch (error) { 1477 | t.pass(error); 1478 | } 1479 | 1480 | receivePingStub.restore(); 1481 | sendPingStub.restore(); 1482 | }); 1483 | 1484 | test.serial('if __sendToChild will reject if no child is found', async (t) => { 1485 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1486 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1487 | 1488 | const config = {}; 1489 | 1490 | const tab = new TabTalk(config, {}); 1491 | 1492 | tab.__children = [ 1493 | { 1494 | id: 'foo', 1495 | ref: { 1496 | postMessage: sinon.spy(), 1497 | }, 1498 | status: TAB_STATUS.OPEN, 1499 | }, 1500 | { 1501 | id: 'bar', 1502 | status: TAB_STATUS.CLOSED, 1503 | }, 1504 | ]; 1505 | 1506 | const id = tab.__children[1].id; 1507 | const event = EVENT.PING_CHILD; 1508 | const data = 'data'; 1509 | 1510 | try { 1511 | await tab.__sendToChild(id, event, data); 1512 | 1513 | t.fail('Should reject'); 1514 | } catch (error) { 1515 | t.is(error.message, 'TabTalk is closed.'); 1516 | } 1517 | 1518 | receivePingStub.restore(); 1519 | sendPingStub.restore(); 1520 | }); 1521 | 1522 | test.serial('if __sendToChildren will iterate over the open children and call __sendToChild', async (t) => { 1523 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1524 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1525 | const sendStub = sinon.stub(TabTalk.prototype, '__sendToChild').resolves(); 1526 | 1527 | const config = {}; 1528 | 1529 | const tab = new TabTalk(config, {}); 1530 | 1531 | tab.__children = [ 1532 | { 1533 | id: 'foo', 1534 | status: TAB_STATUS.OPEN, 1535 | }, 1536 | { 1537 | id: 'bar', 1538 | status: TAB_STATUS.CLOSED, 1539 | }, 1540 | ]; 1541 | 1542 | const event = EVENT.PING_CHILD; 1543 | const data = 'data'; 1544 | 1545 | const result = await tab.__sendToChildren(event, data); 1546 | 1547 | t.true(sendStub.calledOnce); 1548 | t.true(sendStub.calledWith(tab.__children[0].id, event, data)); 1549 | 1550 | t.is(result.length, 1); 1551 | 1552 | sendStub.restore(); 1553 | 1554 | receivePingStub.restore(); 1555 | sendPingStub.restore(); 1556 | }); 1557 | 1558 | test.serial('if __sendToChildren will iterate over the open children and call __sendToChild with defaults', async (t) => { 1559 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1560 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1561 | const sendStub = sinon.stub(TabTalk.prototype, '__sendToChild').resolves(); 1562 | 1563 | const config = {}; 1564 | 1565 | const tab = new TabTalk(config, {}); 1566 | 1567 | tab.__children = [ 1568 | { 1569 | id: 'foo', 1570 | status: TAB_STATUS.OPEN, 1571 | }, 1572 | { 1573 | id: 'bar', 1574 | status: TAB_STATUS.CLOSED, 1575 | }, 1576 | ]; 1577 | 1578 | const event = EVENT.PING_CHILD; 1579 | 1580 | const result = await tab.__sendToChildren(event); 1581 | 1582 | t.true(sendStub.calledOnce); 1583 | t.true(sendStub.calledWith(tab.__children[0].id, event, null)); 1584 | 1585 | t.is(result.length, 1); 1586 | 1587 | sendStub.restore(); 1588 | 1589 | receivePingStub.restore(); 1590 | sendPingStub.restore(); 1591 | }); 1592 | 1593 | test.serial('if __sendToParent will send the encrypted message to the parent via postMessage', async (t) => { 1594 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1595 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1596 | 1597 | const config = {}; 1598 | 1599 | const tab = new TabTalk(config, {}); 1600 | 1601 | tab.__children = [ 1602 | { 1603 | id: 'foo', 1604 | status: TAB_STATUS.OPEN, 1605 | }, 1606 | { 1607 | id: 'bar', 1608 | status: TAB_STATUS.CLOSED, 1609 | }, 1610 | ]; 1611 | 1612 | tab.parent = { 1613 | postMessage: sinon.spy(), 1614 | }; 1615 | 1616 | const event = EVENT.PING_PARENT; 1617 | const data = 'data'; 1618 | 1619 | await tab.__sendToParent(event, data); 1620 | 1621 | t.true(tab.parent.postMessage.calledOnce); 1622 | 1623 | const [message, origin] = tab.parent.postMessage.args[0]; 1624 | 1625 | const parsedMessage = JSON.parse(message); 1626 | 1627 | const decryptedData = await decrypt(parsedMessage.data, tab.config.encryptionKey); 1628 | 1629 | t.deepEqual(decryptedData, data); 1630 | t.is(origin, tab.config.origin); 1631 | 1632 | receivePingStub.restore(); 1633 | sendPingStub.restore(); 1634 | }); 1635 | 1636 | test.serial('if __sendToParent will send the encrypted message to the parent via postMessage with defaults', async (t) => { 1637 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1638 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1639 | 1640 | const config = {}; 1641 | 1642 | const tab = new TabTalk(config, {}); 1643 | 1644 | tab.__children = [ 1645 | { 1646 | id: 'foo', 1647 | status: TAB_STATUS.OPEN, 1648 | }, 1649 | { 1650 | id: 'bar', 1651 | status: TAB_STATUS.CLOSED, 1652 | }, 1653 | ]; 1654 | 1655 | tab.parent = { 1656 | postMessage: sinon.spy(), 1657 | }; 1658 | 1659 | const event = EVENT.PING_PARENT; 1660 | 1661 | await tab.__sendToParent(event); 1662 | 1663 | t.true(tab.parent.postMessage.calledOnce); 1664 | 1665 | const [message, origin] = tab.parent.postMessage.args[0]; 1666 | 1667 | const parsedMessage = JSON.parse(message); 1668 | 1669 | const decryptedData = await decrypt(parsedMessage.data, tab.config.encryptionKey); 1670 | 1671 | t.deepEqual(decryptedData, null); 1672 | t.is(origin, tab.config.origin); 1673 | 1674 | receivePingStub.restore(); 1675 | sendPingStub.restore(); 1676 | }); 1677 | 1678 | test.serial('if __sendToParent will reject if there is no parent', async (t) => { 1679 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1680 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1681 | 1682 | const config = {}; 1683 | 1684 | const tab = new TabTalk(config, {}); 1685 | 1686 | tab.__children = [ 1687 | { 1688 | id: 'foo', 1689 | status: TAB_STATUS.OPEN, 1690 | }, 1691 | { 1692 | id: 'bar', 1693 | status: TAB_STATUS.CLOSED, 1694 | }, 1695 | ]; 1696 | 1697 | const event = EVENT.PING_PARENT; 1698 | const data = 'data'; 1699 | 1700 | try { 1701 | await tab.__sendToParent(event, data); 1702 | 1703 | t.fail('Should reject'); 1704 | } catch (error) { 1705 | t.is(error.message, 'Parent could not be found.'); 1706 | } 1707 | 1708 | receivePingStub.restore(); 1709 | sendPingStub.restore(); 1710 | }); 1711 | 1712 | test('if __setReceivePingInterval will set the interval to set the tab status to closed if timed out', (t) => { 1713 | const receivePingInterval = 123; 1714 | 1715 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(receivePingInterval); 1716 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1717 | 1718 | const config = {}; 1719 | 1720 | const tab = new TabTalk(config, {}); 1721 | 1722 | tab.__children = [ 1723 | { 1724 | id: 'foo', 1725 | lastCheckin: 123456789000, 1726 | status: TAB_STATUS.OPEN, 1727 | }, 1728 | { 1729 | id: 'foo', 1730 | lastCheckin: Date.now() + tab.config.pingInterval, 1731 | status: TAB_STATUS.OPEN, 1732 | }, 1733 | { 1734 | id: 'bar', 1735 | status: TAB_STATUS.CLOSED, 1736 | }, 1737 | ]; 1738 | 1739 | receivePingStub.restore(); 1740 | 1741 | const clearIntervalStub = sinon.stub(global, 'clearInterval'); 1742 | const setIntervalStub = sinon.stub(global, 'setInterval').returns(receivePingInterval); 1743 | 1744 | tab.__setReceivePingInterval(); 1745 | 1746 | t.true(clearIntervalStub.calledOnce); 1747 | 1748 | clearIntervalStub.restore(); 1749 | 1750 | t.true(setIntervalStub.calledOnce); 1751 | 1752 | const [fn, interval] = setIntervalStub.args[0]; 1753 | 1754 | fn(); 1755 | 1756 | t.is(tab.openChildren.length, 1); 1757 | 1758 | setIntervalStub.restore(); 1759 | 1760 | t.is(interval, tab.config.pingInterval); 1761 | 1762 | sendPingStub.restore(); 1763 | }); 1764 | 1765 | test('if __setSendPingInterval will set the interval to notify parent and children', (t) => { 1766 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1767 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1768 | 1769 | const sendParentStub = sinon.stub(TabTalk.prototype, '__sendToParent'); 1770 | const sendChildrenStub = sinon.stub(TabTalk.prototype, '__sendToChildren'); 1771 | 1772 | const config = {}; 1773 | 1774 | const tab = new TabTalk(config, {}); 1775 | 1776 | tab.__children = [ 1777 | { 1778 | id: 'foo', 1779 | lastCheckin: 123456789000, 1780 | status: TAB_STATUS.OPEN, 1781 | }, 1782 | { 1783 | id: 'bar', 1784 | status: TAB_STATUS.CLOSED, 1785 | }, 1786 | ]; 1787 | tab.parent = {}; 1788 | 1789 | sendPingStub.restore(); 1790 | 1791 | const clearIntervalStub = sinon.stub(global, 'clearInterval'); 1792 | const setIntervalStub = sinon.stub(global, 'setInterval').returns(345); 1793 | 1794 | tab.__setSendPingInterval(); 1795 | 1796 | t.true(clearIntervalStub.calledOnce); 1797 | 1798 | clearIntervalStub.restore(); 1799 | 1800 | t.true(setIntervalStub.calledOnce); 1801 | 1802 | const [fn, interval] = setIntervalStub.args[0]; 1803 | 1804 | fn(); 1805 | 1806 | t.true(sendParentStub.calledOnce); 1807 | t.true(sendParentStub.calledWith(EVENT.PING_PARENT, tab.lastCheckin)); 1808 | 1809 | sendParentStub.restore(); 1810 | 1811 | t.true(sendChildrenStub.calledOnce); 1812 | t.true(sendChildrenStub.calledWith(EVENT.PING_CHILD, tab.lastCheckin)); 1813 | 1814 | sendChildrenStub.restore(); 1815 | 1816 | t.is(interval, tab.config.pingInterval); 1817 | 1818 | setIntervalStub.restore(); 1819 | 1820 | receivePingStub.restore(); 1821 | }); 1822 | 1823 | test('if __setSendPingInterval will set the interval to not notify anyone if no parent or children', (t) => { 1824 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1825 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1826 | 1827 | const sendParentStub = sinon.stub(TabTalk.prototype, '__sendToParent'); 1828 | const sendChildrenStub = sinon.stub(TabTalk.prototype, '__sendToChildren'); 1829 | 1830 | const config = {}; 1831 | 1832 | const tab = new TabTalk(config, {}); 1833 | 1834 | tab.__children = []; 1835 | tab.parent = null; 1836 | 1837 | sendPingStub.restore(); 1838 | 1839 | const clearIntervalStub = sinon.stub(global, 'clearInterval'); 1840 | const setIntervalStub = sinon.stub(global, 'setInterval').returns(345); 1841 | 1842 | tab.__setSendPingInterval(); 1843 | 1844 | t.true(clearIntervalStub.calledOnce); 1845 | 1846 | clearIntervalStub.restore(); 1847 | 1848 | t.true(setIntervalStub.calledOnce); 1849 | 1850 | const [fn, interval] = setIntervalStub.args[0]; 1851 | 1852 | fn(); 1853 | 1854 | t.true(sendParentStub.notCalled); 1855 | 1856 | sendParentStub.restore(); 1857 | 1858 | t.true(sendChildrenStub.notCalled); 1859 | 1860 | sendChildrenStub.restore(); 1861 | 1862 | t.is(interval, tab.config.pingInterval); 1863 | 1864 | setIntervalStub.restore(); 1865 | 1866 | receivePingStub.restore(); 1867 | }); 1868 | 1869 | test('if close will call close on the tab ref if no id is passed', (t) => { 1870 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1871 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1872 | 1873 | const removeStub = sinon.stub(TabTalk.prototype, '__removeChild'); 1874 | 1875 | const config = {}; 1876 | 1877 | const tab = new TabTalk(config, {}); 1878 | 1879 | tab.ref.close = sinon.stub(); 1880 | 1881 | const id = null; 1882 | 1883 | tab.close(id); 1884 | 1885 | t.true(tab.ref.close.calledOnce); 1886 | 1887 | t.true(removeStub.notCalled); 1888 | 1889 | removeStub.restore(); 1890 | 1891 | receivePingStub.restore(); 1892 | sendPingStub.restore(); 1893 | }); 1894 | 1895 | test('if close will not close anything if no child is found based on the id', (t) => { 1896 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1897 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1898 | 1899 | const removeStub = sinon.stub(TabTalk.prototype, '__removeChild'); 1900 | 1901 | const config = {}; 1902 | 1903 | const tab = new TabTalk(config, {}); 1904 | 1905 | tab.ref.close = sinon.stub(); 1906 | 1907 | const child = new TabTalk(config, {windowName: 'child'}); 1908 | 1909 | child.parent = tab.ref; 1910 | child.ref.close = sinon.stub(); 1911 | 1912 | tab.__children = [child]; 1913 | 1914 | const id = 'otherId'; 1915 | 1916 | tab.close(id); 1917 | 1918 | t.true(tab.ref.close.notCalled); 1919 | 1920 | t.true(removeStub.notCalled); 1921 | 1922 | t.true(child.ref.close.notCalled); 1923 | 1924 | removeStub.restore(); 1925 | 1926 | receivePingStub.restore(); 1927 | sendPingStub.restore(); 1928 | }); 1929 | 1930 | test('if close will not close anything if the child found is already closed', (t) => { 1931 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1932 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1933 | 1934 | const removeStub = sinon.stub(TabTalk.prototype, '__removeChild'); 1935 | 1936 | const config = {}; 1937 | 1938 | const tab = new TabTalk(config, {}); 1939 | 1940 | tab.ref.close = sinon.stub(); 1941 | 1942 | const child = new TabTalk(config, {windowName: 'child'}); 1943 | 1944 | child.parent = tab.ref; 1945 | child.ref.close = sinon.stub(); 1946 | child.status = TAB_STATUS.CLOSED; 1947 | 1948 | tab.__children = [child]; 1949 | 1950 | const id = tab.__children[0].id; 1951 | 1952 | tab.close(id); 1953 | 1954 | t.true(tab.ref.close.notCalled); 1955 | 1956 | t.true(child.ref.close.notCalled); 1957 | 1958 | t.true(removeStub.notCalled); 1959 | 1960 | removeStub.restore(); 1961 | 1962 | receivePingStub.restore(); 1963 | sendPingStub.restore(); 1964 | }); 1965 | 1966 | test('if close will close the child if found and open', (t) => { 1967 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 1968 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 1969 | 1970 | const removeStub = sinon.stub(TabTalk.prototype, '__removeChild'); 1971 | 1972 | const config = {}; 1973 | 1974 | const tab = new TabTalk(config, {}); 1975 | 1976 | tab.ref.close = sinon.stub(); 1977 | 1978 | const child = new TabTalk(config, {windowName: 'child'}); 1979 | 1980 | child.parent = tab.ref; 1981 | child.ref.close = sinon.stub(); 1982 | 1983 | tab.__children = [child]; 1984 | 1985 | const id = tab.__children[0].id; 1986 | 1987 | tab.close(id); 1988 | 1989 | t.true(tab.ref.close.calledOnce); 1990 | 1991 | t.true(child.ref.close.calledOnce); 1992 | 1993 | t.true(child.ref.close.calledOnce); 1994 | 1995 | t.is(child.status, TAB_STATUS.CLOSED); 1996 | 1997 | t.true(removeStub.notCalled); 1998 | 1999 | removeStub.restore(); 2000 | 2001 | receivePingStub.restore(); 2002 | sendPingStub.restore(); 2003 | }); 2004 | 2005 | test('if close will close the child if found and open and remove if requested in the config', (t) => { 2006 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 2007 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 2008 | 2009 | const removeStub = sinon.stub(TabTalk.prototype, '__removeChild'); 2010 | 2011 | const config = { 2012 | removeOnClosed: true, 2013 | }; 2014 | 2015 | const tab = new TabTalk(config, {}); 2016 | 2017 | tab.ref.close = sinon.stub(); 2018 | 2019 | const child = new TabTalk(config, {windowName: 'child'}); 2020 | 2021 | child.parent = tab.ref; 2022 | child.ref.close = sinon.stub(); 2023 | 2024 | tab.__children = [child]; 2025 | 2026 | const id = tab.__children[0].id; 2027 | 2028 | tab.close(id); 2029 | 2030 | t.true(tab.ref.close.calledOnce); 2031 | 2032 | t.true(child.ref.close.calledOnce); 2033 | 2034 | t.true(child.ref.close.calledOnce); 2035 | 2036 | t.is(child.status, TAB_STATUS.CLOSED); 2037 | 2038 | t.true(removeStub.calledOnce); 2039 | t.true(removeStub.calledWith(child)); 2040 | 2041 | removeStub.restore(); 2042 | 2043 | receivePingStub.restore(); 2044 | sendPingStub.restore(); 2045 | }); 2046 | 2047 | test.serial('if open will open the window and add the child', async (t) => { 2048 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 2049 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 2050 | 2051 | const childWindow = { 2052 | name: '', 2053 | }; 2054 | 2055 | const windowOpenStub = sinon.stub(window, 'open').returns(childWindow); 2056 | const addStub = sinon.stub(TabTalk.prototype, '__addChild'); 2057 | 2058 | const config = {}; 2059 | 2060 | const tab = new TabTalk(config, {}); 2061 | 2062 | const openConfig = { 2063 | config, 2064 | url: 'http://www.example.com', 2065 | }; 2066 | 2067 | const child = await tab.open(openConfig); 2068 | 2069 | t.true(windowOpenStub.calledOnce); 2070 | t.true(windowOpenStub.calledWith(openConfig.url, '_blank', openConfig.windowOptions)); 2071 | 2072 | windowOpenStub.restore(); 2073 | 2074 | t.is(childWindow[TAB_REFERENCE_KEY], child); 2075 | t.is(childWindow.name, utils.getChildWindowName(child.id, tab.id)); 2076 | 2077 | t.true(addStub.calledOnce); 2078 | t.true(addStub.calledWith(child)); 2079 | 2080 | addStub.restore(); 2081 | 2082 | receivePingStub.restore(); 2083 | sendPingStub.restore(); 2084 | }); 2085 | 2086 | test.serial('if open will open the window and add the child with defaults', async (t) => { 2087 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 2088 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 2089 | 2090 | const childWindow = { 2091 | name: '', 2092 | }; 2093 | 2094 | const windowOpenStub = sinon.stub(window, 'open').returns(childWindow); 2095 | const addStub = sinon.stub(TabTalk.prototype, '__addChild'); 2096 | 2097 | const config = {}; 2098 | 2099 | const tab = new TabTalk(config, {}); 2100 | 2101 | const openConfig = { 2102 | url: 'http://www.example.com', 2103 | }; 2104 | 2105 | const child = await tab.open(openConfig); 2106 | 2107 | t.true(windowOpenStub.calledOnce); 2108 | t.true(windowOpenStub.calledWith(openConfig.url, '_blank', openConfig.windowOptions)); 2109 | 2110 | windowOpenStub.restore(); 2111 | 2112 | t.is(childWindow[TAB_REFERENCE_KEY], child); 2113 | t.is(childWindow.name, utils.getChildWindowName(child.id, tab.id)); 2114 | 2115 | t.true(addStub.calledOnce); 2116 | t.true(addStub.calledWith(child)); 2117 | 2118 | addStub.restore(); 2119 | 2120 | receivePingStub.restore(); 2121 | sendPingStub.restore(); 2122 | }); 2123 | 2124 | test('if sendToChild will call __sendToChild with the correct event', (t) => { 2125 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 2126 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 2127 | const sendStub = sinon.stub(TabTalk.prototype, '__sendToChild'); 2128 | 2129 | const config = {}; 2130 | 2131 | const tab = new TabTalk(config, {}); 2132 | 2133 | sendStub.reset(); 2134 | 2135 | const id = 'id'; 2136 | const data = 'data'; 2137 | 2138 | tab.sendToChild(id, data); 2139 | 2140 | t.true(sendStub.calledOnce); 2141 | t.true(sendStub.calledWith(id, EVENT.PARENT_COMMUNICATION, data)); 2142 | 2143 | sendStub.restore(); 2144 | 2145 | receivePingStub.restore(); 2146 | sendPingStub.restore(); 2147 | }); 2148 | 2149 | test('if sendToChild will call __sendToChild with the correct event with defaults', (t) => { 2150 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 2151 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 2152 | const sendStub = sinon.stub(TabTalk.prototype, '__sendToChild'); 2153 | 2154 | const config = {}; 2155 | 2156 | const tab = new TabTalk(config, {}); 2157 | 2158 | sendStub.reset(); 2159 | 2160 | const id = 'id'; 2161 | 2162 | tab.sendToChild(id); 2163 | 2164 | t.true(sendStub.calledOnce); 2165 | t.true(sendStub.calledWith(id, EVENT.PARENT_COMMUNICATION, null)); 2166 | 2167 | sendStub.restore(); 2168 | 2169 | receivePingStub.restore(); 2170 | sendPingStub.restore(); 2171 | }); 2172 | 2173 | test('if sendToChildren will call __sendToChildren with the correct event', (t) => { 2174 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 2175 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 2176 | const sendStub = sinon.stub(TabTalk.prototype, '__sendToChildren'); 2177 | 2178 | const config = {}; 2179 | 2180 | const tab = new TabTalk(config, {}); 2181 | 2182 | sendStub.reset(); 2183 | 2184 | const data = 'data'; 2185 | 2186 | tab.sendToChildren(data); 2187 | 2188 | t.true(sendStub.calledOnce); 2189 | t.true(sendStub.calledWith(EVENT.PARENT_COMMUNICATION, data)); 2190 | 2191 | sendStub.restore(); 2192 | 2193 | receivePingStub.restore(); 2194 | sendPingStub.restore(); 2195 | }); 2196 | 2197 | test('if sendToChildren will call __sendToChildren with the correct event and defaults', (t) => { 2198 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 2199 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 2200 | const sendStub = sinon.stub(TabTalk.prototype, '__sendToChildren'); 2201 | 2202 | const config = {}; 2203 | 2204 | const tab = new TabTalk(config, {}); 2205 | 2206 | sendStub.reset(); 2207 | 2208 | tab.sendToChildren(); 2209 | 2210 | t.true(sendStub.calledOnce); 2211 | t.true(sendStub.calledWith(EVENT.PARENT_COMMUNICATION, null)); 2212 | 2213 | sendStub.restore(); 2214 | 2215 | receivePingStub.restore(); 2216 | sendPingStub.restore(); 2217 | }); 2218 | 2219 | test('if sendToChildren will call __sendToChildren with the correct event and defaults', (t) => { 2220 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 2221 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 2222 | const sendStub = sinon.stub(TabTalk.prototype, '__sendToChildren'); 2223 | 2224 | const config = {}; 2225 | 2226 | const tab = new TabTalk(config, {}); 2227 | 2228 | sendStub.reset(); 2229 | 2230 | tab.sendToChildren(); 2231 | 2232 | t.true(sendStub.calledOnce); 2233 | t.true(sendStub.calledWith(EVENT.PARENT_COMMUNICATION, null)); 2234 | 2235 | sendStub.restore(); 2236 | 2237 | receivePingStub.restore(); 2238 | sendPingStub.restore(); 2239 | }); 2240 | 2241 | test('if sendToParent will call __sendToParent with the correct event', (t) => { 2242 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 2243 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 2244 | const sendStub = sinon.stub(TabTalk.prototype, '__sendToParent'); 2245 | 2246 | const config = {}; 2247 | 2248 | const tab = new TabTalk(config, {}); 2249 | 2250 | sendStub.reset(); 2251 | 2252 | const data = 'data'; 2253 | 2254 | tab.sendToParent(data); 2255 | 2256 | t.true(sendStub.calledOnce); 2257 | t.true(sendStub.calledWith(EVENT.CHILD_COMMUNICATION, data)); 2258 | 2259 | sendStub.restore(); 2260 | 2261 | receivePingStub.restore(); 2262 | sendPingStub.restore(); 2263 | }); 2264 | 2265 | test('if sendToParent will call __sendToParent with the correct event with defaults', (t) => { 2266 | const receivePingStub = sinon.stub(TabTalk.prototype, '__setReceivePingInterval').returns(123); 2267 | const sendPingStub = sinon.stub(TabTalk.prototype, '__setSendPingInterval').returns(234); 2268 | const sendStub = sinon.stub(TabTalk.prototype, '__sendToParent'); 2269 | 2270 | const config = {}; 2271 | 2272 | const tab = new TabTalk(config, {}); 2273 | 2274 | sendStub.reset(); 2275 | 2276 | tab.sendToParent(); 2277 | 2278 | t.true(sendStub.calledOnce); 2279 | t.true(sendStub.calledWith(EVENT.CHILD_COMMUNICATION, null)); 2280 | 2281 | sendStub.restore(); 2282 | 2283 | receivePingStub.restore(); 2284 | sendPingStub.restore(); 2285 | }); 2286 | -------------------------------------------------------------------------------- /test/helpers/setup-browser-env.js: -------------------------------------------------------------------------------- 1 | import browserEnv from 'browser-env'; 2 | import MockWebStorage from 'mock-webstorage'; 3 | import webcrypto from '@trust/webcrypto'; 4 | import { 5 | TextDecoder, 6 | TextEncoder 7 | } from 'util'; 8 | 9 | browserEnv(); 10 | 11 | global.sessionStorage = window.sessionStorage = new MockWebStorage(); 12 | 13 | window.crypto = webcrypto; 14 | window.Promise = global.Promise; 15 | 16 | global.TextDecoder = window.TextDecoder = TextDecoder; 17 | global.TextEncoder = window.TextEncoder = TextEncoder; 18 | -------------------------------------------------------------------------------- /test/sessionStorage.js: -------------------------------------------------------------------------------- 1 | // test 2 | import test from 'ava'; 3 | import sinon from 'sinon'; 4 | 5 | // src 6 | import * as storage from 'src/sessionStorage'; 7 | 8 | test('if getStorageData will get the storage data if it exists', (t) => { 9 | const key = 'key'; 10 | const data = JSON.stringify({some: 'data'}); 11 | 12 | const getStub = sinon.stub(window.sessionStorage, 'getItem').returns(data); 13 | 14 | const result = storage.getStorageData(key); 15 | 16 | t.true(getStub.calledOnce); 17 | t.true(getStub.calledWith(key)); 18 | 19 | getStub.restore(); 20 | 21 | t.deepEqual(result, JSON.parse(data)); 22 | }); 23 | 24 | test('if getStorageData will get null if data does not exist', (t) => { 25 | const key = 'key'; 26 | 27 | const getStub = sinon.stub(window.sessionStorage, 'getItem').returns(null); 28 | 29 | const result = storage.getStorageData(key); 30 | 31 | t.true(getStub.calledOnce); 32 | t.true(getStub.calledWith(key)); 33 | 34 | getStub.restore(); 35 | 36 | t.deepEqual(result, null); 37 | }); 38 | 39 | test('if removeStorageData will remove the item from storage', (t) => { 40 | const key = 'key'; 41 | 42 | const removeStub = sinon.stub(window.sessionStorage, 'removeItem'); 43 | 44 | storage.removeStorageData(key); 45 | 46 | t.true(removeStub.calledOnce); 47 | t.true(removeStub.calledWith(key)); 48 | 49 | removeStub.restore(); 50 | }); 51 | 52 | test('if setStorageData will set the storage data', (t) => { 53 | const key = 'key'; 54 | const data = {some: 'data'}; 55 | 56 | const getStub = sinon.stub(window.sessionStorage, 'setItem'); 57 | 58 | storage.setStorageData(key, data); 59 | 60 | t.true(getStub.calledOnce); 61 | t.true(getStub.calledWith(key, JSON.stringify(data))); 62 | 63 | getStub.restore(); 64 | }); 65 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | // test 2 | import test from 'ava'; 3 | 4 | // src 5 | import * as utils from 'src/utils'; 6 | import { 7 | DEFAULT_CONFIG, 8 | SESSION_STORAGE_KEY 9 | } from 'src/constants'; 10 | 11 | test('if map will perform the same function as the native map', (t) => { 12 | const array = [1, 2, 3, 4, 5, 6, 7, 8]; 13 | const fn = (value) => value * value; 14 | 15 | const result = utils.map(fn, array); 16 | 17 | t.deepEqual(result, array.map(fn)); 18 | }); 19 | 20 | test('if filter will perform the same function as the native filter', (t) => { 21 | const array = [1, 2, 3, 4, 5, 6, 7, 8]; 22 | const fn = (value) => value % 2 === 0; 23 | 24 | const result = utils.filter(fn, array); 25 | 26 | t.deepEqual(result, array.filter(fn)); 27 | }); 28 | 29 | test('if reduce will perform the same function as the native reduce', (t) => { 30 | const array = [1, 2, 3, 4, 5, 6, 7, 8]; 31 | const fn = (sum, value) => sum + value; 32 | 33 | const result = utils.reduce(fn, 0, array); 34 | 35 | t.deepEqual(result, array.reduce(fn, 0)); 36 | }); 37 | 38 | test('if assign will perform the same function as the native assign', (t) => { 39 | const source1 = {foo: 'bar'}; 40 | const source2 = undefined; 41 | const source3 = {baz: 'quz'}; 42 | const target = {}; 43 | 44 | const result = utils.assign(target, source1, source2, source3); 45 | 46 | t.deepEqual(result, Object.assign(target, source1, source2, source3)); 47 | }); 48 | 49 | test('if find will perform the same function as the native find', (t) => { 50 | const array = [1, 2, 3, 4, 5, 6, 7, 8]; 51 | const fn = (value) => value % 2 === 0; 52 | 53 | const result = utils.find(fn, array); 54 | 55 | t.deepEqual(result, array.find(fn)); 56 | }); 57 | 58 | test('if findChildTab will find the correct child tab', (t) => { 59 | const children = [{id: 'foo'}, {id: 'bar'}, {id: 'baz'}]; 60 | const {id} = children[1]; 61 | 62 | const result = utils.findChildTab(children, id); 63 | 64 | t.is(result, children[1]); 65 | }); 66 | 67 | test('if getChildWindowName will compile the windowName correctly', (t) => { 68 | const childId = 'childId'; 69 | const parentId = 'parentId'; 70 | 71 | const result = utils.getChildWindowName(childId, parentId); 72 | 73 | t.is(result, `${SESSION_STORAGE_KEY}:CHILD_${childId}_OF_${parentId}`); 74 | }); 75 | 76 | test('if getHasTimedOut will return true if the childs last checkin was more than the interval + buffer', (t) => { 77 | const child = { 78 | lastCheckin: Date.now() - DEFAULT_CONFIG.pingInterval - DEFAULT_CONFIG.pingCheckinBuffer - 1000, 79 | }; 80 | 81 | t.true(utils.getHasTimedOut(child, DEFAULT_CONFIG)); 82 | }); 83 | 84 | test('if getHasTimedOut will return false if the childs last checkin was less than the interval + buffer', (t) => { 85 | const child = { 86 | lastCheckin: Date.now() - DEFAULT_CONFIG.pingInterval - DEFAULT_CONFIG.pingCheckinBuffer + 1000, 87 | }; 88 | 89 | t.false(utils.getHasTimedOut(child, DEFAULT_CONFIG)); 90 | }); 91 | 92 | test('if getHasTimedOut will return true if the childhas never checked in but created was more than the registration buffer', (t) => { 93 | const child = { 94 | created: Date.now() - DEFAULT_CONFIG.registrationBuffer - 1000, 95 | }; 96 | 97 | t.true(utils.getHasTimedOut(child, DEFAULT_CONFIG)); 98 | }); 99 | 100 | test('if getHasTimedOut will return false if the childhas never checked in but created was less than the registration buffer', (t) => { 101 | const child = { 102 | created: Date.now() - DEFAULT_CONFIG.registrationBuffer + 1000, 103 | }; 104 | 105 | t.false(utils.getHasTimedOut(child, DEFAULT_CONFIG)); 106 | }); 107 | -------------------------------------------------------------------------------- /webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const path = require('path'); 5 | const webpack = require('webpack'); 6 | 7 | const defaultConfig = require('./webpack.config'); 8 | 9 | const PORT = 3000; 10 | const ROOT = path.join(__dirname, '..'); 11 | 12 | module.exports = Object.assign({}, defaultConfig, { 13 | devServer: { 14 | compress: true, 15 | contentBase: path.join(ROOT, 'dist'), 16 | hot: false, 17 | lazy: false, 18 | overlay: true, 19 | port: PORT, 20 | stats: { 21 | colors: true, 22 | progress: true, 23 | }, 24 | }, 25 | 26 | entry: [path.join(ROOT, 'dev', 'index.js')], 27 | 28 | externals: undefined, 29 | 30 | module: Object.assign({}, defaultConfig.module, { 31 | rules: defaultConfig.module.rules.map((loaderObject) => { 32 | if (loaderObject.loader === 'eslint-loader') { 33 | return Object.assign({}, loaderObject, { 34 | options: Object.assign({}, loaderObject.options, { 35 | emitError: undefined, 36 | failOnWarning: false, 37 | }), 38 | }); 39 | } 40 | 41 | if (loaderObject.loader === 'babel-loader') { 42 | return Object.assign({}, loaderObject, { 43 | options: Object.assign({}, loaderObject.options, { 44 | plugins: ['transform-runtime'], 45 | }), 46 | }); 47 | } 48 | 49 | return loaderObject; 50 | }), 51 | }), 52 | 53 | output: Object.assign({}, defaultConfig.output, { 54 | publicPath: `http://localhost:${PORT}/`, 55 | }), 56 | 57 | plugins: [...defaultConfig.plugins, new HtmlWebpackPlugin(), new webpack.NamedModulesPlugin()], 58 | }); 59 | -------------------------------------------------------------------------------- /webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | const ROOT = path.join(__dirname, '..'); 5 | 6 | module.exports = { 7 | cache: true, 8 | 9 | devtool: '#cheap-module-source-map', 10 | 11 | entry: './src/index.js', 12 | 13 | externals: { 14 | react: { 15 | amd: 'react', 16 | commonjs: 'react', 17 | commonjs2: 'react', 18 | root: 'React', 19 | }, 20 | 'react-dom': { 21 | amd: 'react-dom', 22 | commonjs: 'react-dom', 23 | commonjs2: 'react-dom', 24 | root: 'ReactDOM', 25 | }, 26 | }, 27 | 28 | mode: 'development', 29 | 30 | module: { 31 | rules: [ 32 | { 33 | enforce: 'pre', 34 | include: [path.resolve(ROOT, 'src')], 35 | loader: 'eslint-loader', 36 | options: { 37 | configFile: '.eslintrc', 38 | emitError: true, 39 | failOnError: true, 40 | failOnWarning: true, 41 | formatter: require('eslint-friendly-formatter'), 42 | }, 43 | test: /\.js$/, 44 | }, 45 | { 46 | include: [path.resolve(ROOT, 'dev'), path.resolve(ROOT, 'src')], 47 | loader: 'babel-loader', 48 | options: { 49 | cacheDirectory: true, 50 | }, 51 | test: /\.js$/, 52 | }, 53 | ], 54 | }, 55 | 56 | output: { 57 | filename: 'tabtalk.js', 58 | library: 'tabtalk', 59 | libraryTarget: 'umd', 60 | path: path.join(ROOT, 'dist'), 61 | umdNamedDefine: true, 62 | }, 63 | 64 | plugins: [new webpack.EnvironmentPlugin(['NODE_ENV'])], 65 | }; 66 | -------------------------------------------------------------------------------- /webpack/webpack.config.minified.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | const OptimizeJsPlugin = require('optimize-js-plugin'); 5 | 6 | const defaultConfig = require('./webpack.config'); 7 | 8 | module.exports = Object.assign({}, defaultConfig, { 9 | devtool: undefined, 10 | 11 | mode: 'production', 12 | 13 | output: Object.assign({}, defaultConfig.output, { 14 | filename: 'tabtalk.min.js', 15 | }), 16 | 17 | plugins: defaultConfig.plugins.concat([ 18 | new webpack.LoaderOptionsPlugin({ 19 | debug: false, 20 | minimize: true, 21 | }), 22 | new OptimizeJsPlugin({ 23 | sourceMap: false, 24 | }), 25 | ]), 26 | }); 27 | --------------------------------------------------------------------------------