├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── addon-container.es5.js ├── addon-container.es5.min.js ├── addon-container.es5.min.js.map ├── addon-container.js ├── addon-container.min.js ├── addon-container.min.js.map ├── addon.es5.js ├── addon.es5.min.js ├── addon.es5.min.js.map ├── addon.js ├── addon.min.js └── addon.min.js.map ├── docker-compose.yml ├── example.html ├── index.js ├── lib ├── addon-container.es5.js ├── addon-container.js ├── addon.es5.js └── addon.js ├── package.json ├── src ├── addon-container.js ├── addon.js ├── api.js └── iframe-resizer-options.js ├── tests ├── .eslintrc.js ├── integration │ ├── addon-container.html │ ├── addon-container.spec.js │ ├── addon.html │ ├── addon.spec.js │ └── bootstrap.js └── unit │ ├── addon-container.spec.js │ ├── addon.spec.js │ └── api.spec.js ├── webpack.config.babel.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-async-to-generator", 4 | "lodash" 5 | ], 6 | "presets": [ 7 | "es2015", 8 | "stage-2" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | max_line_length = 100 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | lib 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['airbnb-base'], 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | tmp 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .idea 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | before_install: 5 | - npm i -g npm@6 6 | cache: yarn 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.25] 4 | - `upgradePremium(plan)` support passing plan param 5 | 6 | ## [0.0.24] 7 | 8 | ### Fixed 9 | - `setLoadingStatus` allow emit empty value 10 | 11 | ## [0.0.23] 12 | 13 | ### Added 14 | - `setLoadingStatus` method to set loading status, param string 15 | 16 | ## [0.0.22] 17 | 18 | ### Added 19 | - `printPage` method to run `window.print()` from dashboard itself. 20 | 21 | ## [0.0.9] 22 | 23 | ### Added 24 | - Travis CI. 25 | 26 | ### Changed 27 | - Change editTransaction to receive id string instead of object. 28 | - Change `scope` to `id` for Addon & AddonContainer initialization. 29 | - Refine Sample Addon. 30 | 31 | ### Fixed 32 | - Fix height calculation method to allow for downsizing. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wealthica Financial Technology Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wealthica.js 2 | 3 | Extend Wealthica by writing your own add-ons and widgets. 4 | 5 | [Wealthica](https://wealthica.com) is an aggregation platform that allows investors to see all their investments in a single Dashboard and get an unbiased view of their wealth. Each day, Wealthica connects to your financial institutions and retrieves accounts balances, positions and transactions. Wealthica is the largest financial aggregator in Canada with support for 200+ Canadian financial institutions and investment platforms. 6 | 7 | This library provides the wrappers needed to setup the communication between the Wealthica Dashboard and 3rd-party add-ons. It aims to be simple to use and allows anyone with basic knowledge of JavaScript to write their own add-on. 8 | 9 | Not sure where to start? Take a [look at the Example Add-on code](https://github.com/wealthica/wealthica.js/blob/master/example.html) and see the instructions below to load it into your Wealthica Dashboard. 10 | 11 | Ready to publish your add-on to the Wealthica Add-ons store? [Contact us](mailto:hello@wealthica.com). 12 | 13 | ## Financial Institutions 14 | 15 | Wealthica supports connecting to the following Canadian financial institutions and investment portals. Support for importing positions (holdings), transactions and account statements varies. [Visit our website](https://wealthica.com) for the most recent list of supported institutions. 16 | 17 | ## Getting started 18 | 19 | 1. Login to your [Wealthica](https://app.wealthica.com). 20 | 2. Install the [Developer Add-on](https://app.wealthica.com/addons/details?id=wealthica/wealthica-dev-addon). After that you will be redirected to the Developer Add-on page. 21 | 3. Click on the button next to the page title to open the Configure modal. 22 | 4. Enter https://wealthica.github.io/wealthica.js/example.html to the Add-on URL field and click Load to load the Example Add-on. 23 | 5. Try some of the sample actions provided in the add-on. 24 | 25 | After trying out the Example Add-on, it's time to make your own add-on: 26 | 27 | 1. Clone Example Add-on or create your own add-on page. 28 | 2. From Wealthica navigate to the Developer Add-on and open the Configure modal and load your add-on. 29 | 3. Take a look at the [APIs](#apis) section below and start writing your add-on. 30 | 31 | For a more advanced example, take a look at our [Wealthica Cryptos Addon code](https://github.com/wealthica/wealthica-cryptos-addon). 32 | 33 | ## Installation 34 | 35 | ``` 36 | npm install @wealthica/wealthica.js --save 37 | ``` 38 | 39 | Then include this in your add-on page: 40 | 41 | ``` 42 | 43 | ``` 44 | 45 | ## APIs 46 | 47 | ### class: Addon 48 | 49 | The `Addon` class is intended to be used by add-ons to setup their side of communication. 50 | 51 | #### new Addon([options]) 52 | 53 | ``` 54 | var addon = new Addon({ 55 | // (optional) The 'id' of the add-on / widget. 56 | // This is only required in the add-on release preparation process. 57 | // For add-on development with the Developer Add-on, this should not be set. 58 | id: 'addon-id' | 'addon-id/widgets/widget-id' 59 | }); 60 | ``` 61 | 62 | #### event: 'init' 63 | 64 | Emitted when Wealthica Dashboard has finished setting up the communication on its side. At this time add-on should use the passed-in options (filters, language, etc.) to finalize its rendering. 65 | 66 | ``` 67 | addon.on('init', function (options) { 68 | console.log(options); 69 | // { 70 | // fromDate: '2018-01-01', 71 | // toDate: '2018-04-30', 72 | // language: 'en', 73 | // privateMode: false, 74 | // data: { preferredCurrencies: ['USD', 'CAD', 'GBP'] }, 75 | // ... 76 | // } 77 | }); 78 | ``` 79 | 80 | #### event: 'reload' 81 | 82 | Emitted when there are important changes in Wealthica Dashboard (e.g. user has just added a new institution, or removed an asset). This suggests add-ons reload their data and render accordingly. 83 | 84 | ``` 85 | addon.on('reload', function () { 86 | // Start reloading 87 | }); 88 | ``` 89 | 90 | #### event: 'update' 91 | 92 | Emitted when user updates one of the Dashboard filters. The Dashboard provides common filters including groups, institutions or date range filters. In response add-on should update itself according to received options. 93 | 94 | ``` 95 | addon.on('update', function (options) { 96 | // Update according to the received options 97 | }); 98 | ``` 99 | 100 | #### event: 'institution:sync' 101 | 102 | Emitted when Wealthica Dashboard receives a signal that the institution sync is done. 103 | 104 | ``` 105 | addon.on('institution:sync', function (institutionId) { 106 | // Update synced institution 107 | }); 108 | ``` 109 | 110 | #### addon.addTransaction(attrs) 111 | 112 | This method opens the Add Transaction form on the Dashboard and waits for user to submit the transaction or to close the modal. Pass an optional `attrs` object to pre-populate the transaction form. The `newTransaction` parameter is provided when a new transaction has been created. 113 | 114 | ``` 115 | addon.addTransaction({ description: "Some description" }).then(function (newTransaction) { 116 | // The form has been closed 117 | 118 | if (newTransaction) { 119 | // A new transaction has been created 120 | } else { 121 | // Nothing changed 122 | } 123 | }).catch(function (err) { 124 | 125 | }); 126 | ``` 127 | 128 | #### addon.editTransaction(id) 129 | 130 | This method opens the Edit Transaction form on the Dashboard and waits for user to update the transaction or to close the form. The `updatedTransaction` parameter is provided when the transaction has been updated. 131 | 132 | ``` 133 | addon.editTransaction('transaction-id').then(function (updatedTransaction) { 134 | // The form has been closed 135 | 136 | if (updatedTransaction) { 137 | // The transaction has been updated 138 | } else { 139 | // Nothing changed 140 | } 141 | }).catch(function (err) { 142 | 143 | }); 144 | ``` 145 | 146 | #### addon.addInstitution(attrs) 147 | 148 | This method opens the Add Institution form on the Dashboard and waits for user to finish the process or to close the form. 149 | 150 | Pass an optional institution object (`{ type, name }`) to go straight to the institution's credentials form. Otherwise the modal will open at the Select Institution step. 151 | 152 | The `newInstitution` parameter is provided when a new institution has been created. 153 | 154 | ``` 155 | addon.addInstitution({ type: 'demo', name: 'Demo' }).then(function (newInstitution) { 156 | // The form has been closed 157 | 158 | if (newInstitution) { 159 | // A new institution has been created 160 | } else { 161 | // Nothing changed 162 | } 163 | }).catch(function (err) { 164 | 165 | }); 166 | ``` 167 | 168 | #### addon.addInvestment() 169 | 170 | This method opens the Add Investment form on the Dashboard and waits for user to finish the process or to close the form. 171 | 172 | ``` 173 | addon.addInvestment().then(function (result) { 174 | // The form has been closed 175 | 176 | if (result) { 177 | // A new institution, asset or liability has been created 178 | } else { 179 | // Nothing changed 180 | } 181 | }).catch(function (err) { 182 | 183 | }); 184 | ``` 185 | 186 | #### addon.editInstitution(id), addon.editAsset(id) & addon.editLiability(id) 187 | 188 | These methods open the corresponding Edit form (Institution/Asset/Liability) on the Dashboard and wait for user to update the item or to close the form. 189 | 190 | The `updatedItem` parameter is provided when the item has been successfully updated. 191 | 192 | ``` 193 | addon.editInstitution('institution-id').then(function (updatedItem) { 194 | // The form has been closed 195 | 196 | if (updatedItem) { 197 | // The item has been updated 198 | } else { 199 | // Nothing changed 200 | } 201 | }).catch(function (err) { 202 | 203 | }); 204 | ``` 205 | 206 | #### addon.deleteInstitution(id), addon.deleteAsset(id) & addon.deleteLiability(id) 207 | 208 | These methods open the corresponding Delete form (Institution/Asset/Liability) on the Dashboard and wait for user to confirm or to close the form. 209 | 210 | The `deleted` parameter is provided when the item has been successfully deleted. 211 | 212 | ``` 213 | addon.deleteInstitution('institution-id').then(function (deleted) { 214 | // The form has been closed 215 | 216 | if (deleted) { 217 | // The item has been deleted 218 | } else { 219 | // Nothing changed 220 | } 221 | }).catch(function (err) { 222 | 223 | }); 224 | ``` 225 | 226 | #### addon.downloadDocument(id) 227 | 228 | This method triggers download of a Wealthica document 229 | 230 | ``` 231 | addon.downloadDocument('document-id').then(function () { 232 | // The action has been successfully carried out. 233 | }).catch(function (err) { 234 | 235 | }); 236 | ``` 237 | 238 | #### addon.getSharings() 239 | 240 | This method request the list of sharings for the logged in user. 241 | 242 | NOTE: Only available for addons with `"switch_user"` feature flag. 243 | 244 | ``` 245 | addon.getSharings().then(function (sharings) {}).catch(function (err) {}); 246 | ``` 247 | 248 | #### addon.switchUser(id) 249 | 250 | This method switches Dashboard to viewing the user with provided id. 251 | 252 | NOTE: Only available for addons with `"switch_user"` feature flag. 253 | 254 | ``` 255 | addon.switchUser(id).then(function () { 256 | // Successfully switched 257 | }).catch(function (err) {}); 258 | ``` 259 | 260 | #### addon.saveData(data) 261 | 262 | This method allows add-on to persist data to the Wealthica user preferences. You can use this method to persist user configuration options. The add-on will receive this data under the `data` options parameter [the next time it is initialized](#event-init). Each add-on can store up to 100 KB of data (plus 4 KB per widget). Please note data is stored unencrypted in our database and may not be suitable for storing sensitive information. 263 | 264 | ``` 265 | addon.saveData({ preferredCurrencies: ['CAD', 'USD', 'GBP', 'MXN'] }).then(function () { 266 | 267 | }).catch(function (err) { 268 | 269 | }); 270 | ``` 271 | 272 | #### addon.setLoadingStatus(status) 273 | This method sets the loading status, sets the status string - shows the loader, sets an empty string - hides the loader. 274 | 275 | #### addon.printPage() 276 | 277 | This method triggers `window.print()` from dashboard itself. 278 | 279 | #### addon.upgradePremium(plan) 280 | 281 | This method triggers `window.upgradePremium(plan)` popup which suggesting user to upgrade his plan (available options: 'premium' and 'prestige' ) 282 | 283 | ### API helpers 284 | 285 | These are helper functions for requesting API calls. See our API docs for the full list of API endpoints, their parameters and what they do. 286 | 287 | For API endpoints that are not yet supported by the API helpers, see [addon.request](#addonrequestoptions) in the Debug section below. 288 | 289 | #### addon.api.getAssets(query) 290 | 291 | ``` 292 | addon.api.getAssets({ date: '2018-01-01' }) 293 | .then(function (response) { }).catch(function (err) { }); 294 | ``` 295 | 296 | #### addon.api.getCurrencies(query) 297 | 298 | ``` 299 | addon.api.getCurrencies({ from: '2018-01-01', to: '2018-01-31' }) 300 | .then(function (response) { }).catch(function (err) { }); 301 | ``` 302 | 303 | #### addon.api.getInstitutions(query) 304 | 305 | ``` 306 | addon.api.getInstitutions({ date: '2018-01-01' }) 307 | .then(function (response) { }).catch(function (err) { }); 308 | ``` 309 | 310 | #### addon.api.getInstitution(id) 311 | 312 | ``` 313 | addon.api.getInstitution('institution-id') 314 | .then(function (response) { }).catch(function (err) { }); 315 | ``` 316 | 317 | #### addon.api.pollInstitution(id, version) 318 | 319 | ``` 320 | addon.api.pollInstitution('institution-id', 1) 321 | .then(function (response) { }).catch(function (err) { }); 322 | ``` 323 | 324 | #### addon.api.syncInstitution(id) 325 | 326 | ``` 327 | addon.api.syncInstitution('institution-id') 328 | .then(function (response) { }).catch(function (err) { }); 329 | ``` 330 | 331 | #### addon.api.addInstitution(data) 332 | 333 | ``` 334 | addon.api.addInstitution({ 335 | name: 'Demo', 336 | type: 'demo', 337 | credentials: { username: 'wealthica', password: 'wealthica' } 338 | }).then(function (response) { }).catch(function (err) { }); 339 | ``` 340 | 341 | #### addon.api.getLiabilities(query) 342 | 343 | ``` 344 | addon.api.getLiabilities({ date: '2018-01-01' }) 345 | .then(function (response) { }).catch(function (err) { }); 346 | ``` 347 | 348 | #### addon.api.getPositions(query) 349 | 350 | ``` 351 | addon.api.getPositions({ groups: 'id1,id2', institutions: 'id1,id2' }) 352 | .then(function (response) { }).catch(function (err) { }); 353 | ``` 354 | 355 | #### addon.api.getTransactions(query) 356 | 357 | ``` 358 | addon.api.getTransactions({ groups: 'id1,id2', institutions: 'id1,id2' }) 359 | .then(function (response) { }).catch(function (err) { }); 360 | ``` 361 | 362 | #### addon.api.updateTransaction(id, attrs) 363 | 364 | ``` 365 | addon.api.updateTransaction('id', { currency_amount: 123 }) 366 | .then(function (response) { }).catch(function (err) { }); 367 | ``` 368 | 369 | #### addon.api.getUser() 370 | 371 | ``` 372 | addon.api.getUser().then(function (response) { }).catch(function (err) { }); 373 | ``` 374 | 375 | ## Debug 376 | 377 | #### event: 'postMessage' 378 | 379 | Emitted every time the add-on posts a message to the Wealthica Dashboard. 380 | 381 | ``` 382 | addon.on('postMessage', function (origin, message) { 383 | console.log(arguments); 384 | }); 385 | ``` 386 | 387 | Emitted every time the add-on receives a message from the Wealthica Dashboard. 388 | 389 | ``` 390 | addon.on('gotMessage', function (origin, message) { 391 | console.log(arguments); 392 | }); 393 | ``` 394 | 395 | #### addon.request(options) 396 | 397 | This is used to make a request to API endpoints that are not currently supported by `addon.api`. 398 | 399 | ``` 400 | addon.request({ 401 | method: 'GET', 402 | endpoint: 'positions', 403 | query: { 404 | institutions: 'id1,id2', 405 | assets: true, 406 | } 407 | }).then(function (response) { }).catch(function (err) { }); 408 | ``` 409 | 410 | #### addon.setEffectiveUser(id) 411 | 412 | This is used make `addon.request()` calls on behalf of another user who has shared access to the currently logged in user. It has the same effect for any of the `addon.api` helpers which are wrappers around `addon.request()`. 413 | 414 | ``` 415 | addon.setEffectiveUser(userA); 416 | addon.request({...}).then(function (response) { }).catch(function (err) { }); 417 | 418 | addon.setEffectiveUser(userB); 419 | addon.request({...}).then(function (response) { }).catch(function (err) { }); 420 | 421 | // It's advised to clear effectiveUser after finishing on-behalf calls 422 | addon.setEffectiveUser(null); 423 | addon.request({...}).then(function (response) { }).catch(function (err) { }); 424 | ``` 425 | 426 | ## Development 427 | 428 | ### Install 429 | 430 | ``` 431 | yarn install 432 | ``` 433 | 434 | ### Build 435 | 436 | ``` 437 | yarn build 438 | ``` 439 | 440 | ### Test 441 | 442 | ``` 443 | yarn build 444 | yarn test 445 | ``` 446 | 447 | ### Release 448 | ``` 449 | npm version patch # or minor/major 450 | git push --tags 451 | # wait until merged then 452 | npm publish 453 | ``` 454 | -------------------------------------------------------------------------------- /dist/addon-container.es5.min.js: -------------------------------------------------------------------------------- 1 | var AddonContainer;!function(){var e={681:function(e){var t=function(){"use strict";var e=Math.floor(1000001*Math.random()),t={};function n(e){return Array.isArray?Array.isArray(e):-1!=e.constructor.toString().indexOf("Array")}var i={},o=function(e){try{var n=JSON.parse(e.data);if("object"!=typeof n||null===n)throw"malformed"}catch(e){return}var o,r,a,s,c;if(window.ReactNativeWebView?(r="*",o=window.ReactNativeWebView):(r=e.origin,o=e.source),"string"==typeof n.method){var u=n.method.split("::");2==u.length?(a=u[0],c=u[1]):(a="",c=n.method)}if(void 0!==n.id&&(s=n.id),"string"==typeof c){var l=!1;if(t[r]&&t[r][a])for(var f=0;f1)throw"scope may not contain double colons: '::'";u=o.scope}var l=function(){for(var e="",t="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",n=0;n<5;n++)e+=t.charAt(Math.floor(Math.random()*t.length));return e}(),f={},d={},m={},h=!1,g=[],p=function(e,t,s){if("function"==typeof o.gotMessageObserver)try{o.gotMessageObserver(e,JSON.parse(JSON.stringify(s)))}catch(e){r("gotMessageObserver() raised an exception: "+e.toString())}if(s.id&&t)if(r("received request: "+t+"() from "+e+" (id: "+s.id+")"),f[t]){var c=function(e,t,n){var i=!1,o=!1,s=function(t){r("transaction("+e+"): "+t)};return{origin:t,invoke:function(t,i){if(o)s("Warning: invoke called after completion");else if(m[e]){for(var r=!1,c=0;c0)for(var u=0;u=0)throw"params cannot be a recursive data structure containing functions";for(var i in s.push(t),t)if(t.hasOwnProperty(i)){var r=t[i],a=e+(e.length?"/":"")+i;"function"==typeof r?(n[a]=r,o.push(a)):"object"==typeof r&&c(a,r)}s.pop()}},u=t.params?JSON.parse(JSON.stringify(t.params)):void 0;c("",t.params);var l,f,m,h=e++,g=v(t.method),y={id:h,method:g,params:u};o.length&&(y.callbacks=o),d[h]={callbacks:n,error:t.error,success:t.success,timeoutId:t.timeout?(l=h,f=t.timeout,m=g,window.setTimeout((function(){if(d[l]){var e="timeout ("+f+"ms) exceeded on method '"+m+"'";r(e+" for transaction "+l);try{"function"==typeof d[l].error&&d[l].error("timeout_error",e)}catch(e){r("Exception executing timeout handler: "+e)}finally{delete d[l],delete i[l]}}}),f)):null},i[h]=p,r("calling method '"+g+"' with id "+h),w(y,a)},notify:function(e){if(!e)throw"missing arguments to notify function";if(!e.method||"string"!=typeof e.method)throw"'method' argument to notify must be string";var t=v(e.method);r("sending notification: "+t);var n=e.params?JSON.parse(JSON.stringify(e.params)):void 0;w({method:t,params:n},a)},destroy:function(){for(var e in r("Destroying channel: "+l),function(e,n,i){if(t[n]&&t[n][i]){for(var o=t[n][i],r=0;r0;)w(g.shift(),!0);if("function"==typeof o.onReady)try{o.onReady(y)}catch(e){r("Exception in onReady callback: "+e)}})),r("Initiating ready handshake (sending ping)"),window.setTimeout((function(){w({method:v("__ready"),params:"ping"},a)}),0),y}}}();e.exports&&(e.exports=t)},669:function(e,t,n){"use strict";var i=u(n(218)),o=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};l(this,t);var n=f(this,(t.__proto__||Object.getPrototypeOf(t)).call(this));if(n.options=e,!e.iframe)throw new Error("Iframe not defined");return(0,c.iframeResizer)({checkOrigin:!0,heightCalculationMethod:window.ieVersion<=10?"max":"lowestElement",resizeFrom:"child",resizedCallback:function(e){n.emit("iframeResized",e)}},e.iframe),n.channel=r.default.build({window:e.iframe.contentWindow,origin:e.origin||"*",scope:e.id||e.iframe.contentWindow.location.origin,postMessageObserver:function(e,t){n.emit("postMessage",e,t)},gotMessageObserver:function(e,t){n.emit("gotMessage",e,t)}}),["saveData","request","addTransaction","editTransaction","addInstitution","addInvestment","editInstitution","editAsset","editLiability","deleteInstitution","deleteAsset","deleteLiability","downloadDocument","upgradePremium","getSharings","switchUser","printPage","setLoadingStatus"].forEach((function(e){n.channel.bind(e,(function(t,i){var o=e,r=i;t.delayReturn(!0);var a=function(e,n){return e?t.error(e):t.complete(n)};["setLoadingStatus","upgradePremium"].includes(e)?n.emit(o,void 0!==r?r:a,void 0!==r?a:void 0):n.emit(o,r||a,r?a:void 0)}))})),n.channel.call({method:"init",params:e.options,success:function(e){n.emit("init",e)}}),n}return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}(t,e),o(t,[{key:"trigger",value:function(e,t){var n=this,i={eventName:e};return t&&(i.eventData=t),new s.Promise((function(e,t){n.channel.call({method:"_event",params:i,success:e,error:t})}))}},{key:"update",value:function(e){var t=this;return new s.Promise((function(n,o){if(!(0,i.default)(e))throw new Error("Data must be an object");t.channel.call({method:"update",params:e,success:n,error:o})}))}},{key:"reload",value:function(){var e=this;return new s.Promise((function(t,n){e.channel.call({method:"reload",success:t,error:n})}))}},{key:"destroy",value:function(){this.channel.destroy()}}]),t}(a.default);e.exports=d},702:function(e,t,n){ 2 | /*! 3 | * @overview es6-promise - a tiny implementation of Promises/A+. 4 | * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) 5 | * @license Licensed under MIT license 6 | * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE 7 | * @version v4.2.8+1e68dce6 8 | */ 9 | e.exports=function(){"use strict";function e(e){var t=typeof e;return null!==e&&("object"===t||"function"===t)}function t(e){return"function"==typeof e}var i=Array.isArray?Array.isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},o=0,r=void 0,a=void 0,s=function(e,t){b[o]=e,b[o+1]=t,2===(o+=2)&&(a?a(k):_())};function c(e){a=e}function u(e){s=e}var l="undefined"!=typeof window?window:void 0,f=l||{},d=f.MutationObserver||f.WebKitMutationObserver,m="undefined"==typeof self&&"undefined"!=typeof process&&"[object process]"==={}.toString.call(process),h="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel;function g(){return function(){return process.nextTick(k)}}function p(){return void 0!==r?function(){r(k)}:y()}function v(){var e=0,t=new d(k),n=document.createTextNode("");return t.observe(n,{characterData:!0}),function(){n.data=e=++e%2}}function w(){var e=new MessageChannel;return e.port1.onmessage=k,function(){return e.port2.postMessage(0)}}function y(){var e=setTimeout;return function(){return e(k,1)}}var b=new Array(1e3);function k(){for(var e=0;eL?(a&&(clearTimeout(a),a=null),s=e,r=n.apply(i,o),a||(i=o=null)):a||(a=setTimeout(c,t)),r});$(window,"message",Ee),"loading"!==document.readyState&&window.parent.postMessage("[iFrameResizerChild]Ready","*")}function $(e,t,n){"addEventListener"in window?e.addEventListener(t,n,!1):"attachEvent"in window&&e.attachEvent("on"+t,n)}function ee(e,t,n){"removeEventListener"in window?e.removeEventListener(t,n,!1):"detachEvent"in window&&e.detachEvent("on"+t,n)}function te(e){return e.charAt(0).toUpperCase()+e.slice(1)}function ne(e){return S+"["+N+"] "+e}function ie(e){T&&"object"==typeof window.console&&console.log(ne(e))}function oe(e){"object"==typeof window.console&&console.warn(ne(e))}function re(){var e;!function(){function e(e){return"true"===e}var n=_.substr(I).split(":");N=n[0],f=t!==n[1]?Number(n[1]):f,g=t!==n[2]?e(n[2]):g,T=t!==n[3]?e(n[3]):T,E=t!==n[4]?Number(n[4]):E,u=t!==n[6]?e(n[6]):u,d=n[7],k=t!==n[8]?n[8]:k,l=n[9],h=n[10],P=t!==n[11]?Number(n[11]):P,x.enable=t!==n[12]&&e(n[12]),A=t!==n[13]?n[13]:A,J=t!==n[14]?n[14]:J}(),ie("Initialising iFrame ("+location.href+")"),function(){function e(){var e=window.iFrameResizer;ie("Reading data from page: "+JSON.stringify(e)),D="messageCallback"in e?e.messageCallback:D,B="readyCallback"in e?e.readyCallback:B,z="targetOrigin"in e?e.targetOrigin:z,k="heightCalculationMethod"in e?e.heightCalculationMethod:k,J="widthCalculationMethod"in e?e.widthCalculationMethod:J}function t(e,t){return"function"==typeof e&&(ie("Setup custom "+t+"CalcMethod"),Y[t]=e,e="custom"),e}"iFrameResizer"in window&&Object===window.iFrameResizer.constructor&&(e(),k=t(k,"height"),J=t(J,"width"));ie("TargetOrigin for parent set to: "+z)}(),function(){t===d&&(d=f+"px");ae("margin",function(e,t){-1!==t.indexOf("-")&&(oe("Negative CSS value ignored for "+e),t="");return t}("margin",d))}(),ae("background",l),ae("padding",h),(e=document.createElement("div")).style.clear="both",e.style.display="block",document.body.appendChild(e),le(),fe(),document.documentElement.style.height="",document.body.style.height="",ie('HTML & body height set to "auto"'),ie("Enable public methods"),V.parentIFrame={autoResize:function(e){return!0===e&&!1===u?(u=!0,de()):!1===e&&!0===u&&(u=!1,me()),u},close:function(){xe(0,0,"close"),ie("Disable outgoing messages"),R=!1,ie("Remove event listener: Message"),ee(window,"message",Ee),!0===u&&me()},getId:function(){return N},getPageInfo:function(e){"function"==typeof e?(U=e,xe(0,0,"pageInfo")):(U=function(){},xe(0,0,"pageInfoStop"))},moveToAnchor:function(e){x.findTarget(e)},reset:function(){_e("parentIFrame.reset")},scrollTo:function(e,t){xe(t,e,"scrollTo")},scrollToOffset:function(e,t){xe(t,e,"scrollToOffset")},sendMessage:function(e,t){xe(0,0,"message",JSON.stringify(e),t)},setHeightCalculationMethod:function(e){k=e,le()},setWidthCalculationMethod:function(e){J=e,fe()},setTargetOrigin:function(e){ie("Set targetOrigin: "+e),z=e},size:function(e,t){be("size","parentIFrame.size("+(e||"")+(t?","+t:"")+")",e,t)}},de(),x=function(){function e(){return{x:window.pageXOffset!==t?window.pageXOffset:document.documentElement.scrollLeft,y:window.pageYOffset!==t?window.pageYOffset:document.documentElement.scrollTop}}function n(t){var n=t.getBoundingClientRect(),i=e();return{x:parseInt(n.left,10)+parseInt(i.x,10),y:parseInt(n.top,10)+parseInt(i.y,10)}}function i(e){function i(e){var t=n(e);ie("Moving to in page link (#"+o+") at x: "+t.x+" y: "+t.y),xe(t.y,t.x,"scrollToOffset")}var o=e.split("#")[1]||e,r=decodeURIComponent(o),a=document.getElementById(r)||document.getElementsByName(r)[0];t!==a?i(a):(ie("In page link (#"+o+") not found in iFrame, so sending to parent"),xe(0,0,"inPageLink","#"+o))}function o(){""!==location.hash&&"#"!==location.hash&&i(location.href)}function r(){function e(e){function t(e){e.preventDefault(),i(this.getAttribute("href"))}"#"!==e.getAttribute("href")&&$(e,"click",t)}Array.prototype.forEach.call(document.querySelectorAll('a[href^="#"]'),e)}function a(){$(window,"hashchange",o)}function s(){setTimeout(o,v)}function c(){Array.prototype.forEach&&document.querySelectorAll?(ie("Setting up location.hash handlers"),r(),a(),s()):oe("In page linking not fully supported in this browser! (See README.md for IE8 workaround)")}x.enable?c():ie("In page linking not enabled");return{findTarget:i}}(),be("init","Init message from host page"),B()}function ae(e,n){t!==n&&""!==n&&"null"!==n&&(document.body.style[e]=n,ie("Body "+e+' set to "'+n+'"'))}function se(e){var t={add:function(t){function n(){be(e.eventName,e.eventType)}X[t]=n,$(window,t,n)},remove:function(e){var t=X[e];delete X[e],ee(window,e,t)}};e.eventNames&&Array.prototype.map?(e.eventName=e.eventNames[0],e.eventNames.map(t[e.method])):t[e.method](e.eventName),ie(te(e.method)+" event listener: "+e.eventType)}function ce(e){se({method:e,eventType:"Animation Start",eventNames:["animationstart","webkitAnimationStart"]}),se({method:e,eventType:"Animation Iteration",eventNames:["animationiteration","webkitAnimationIteration"]}),se({method:e,eventType:"Animation End",eventNames:["animationend","webkitAnimationEnd"]}),se({method:e,eventType:"Input",eventName:"input"}),se({method:e,eventType:"Mouse Up",eventName:"mouseup"}),se({method:e,eventType:"Mouse Down",eventName:"mousedown"}),se({method:e,eventType:"Orientation Change",eventName:"orientationchange"}),se({method:e,eventType:"Print",eventName:["afterprint","beforeprint"]}),se({method:e,eventType:"Ready State Change",eventName:"readystatechange"}),se({method:e,eventType:"Touch Start",eventName:"touchstart"}),se({method:e,eventType:"Touch End",eventName:"touchend"}),se({method:e,eventType:"Touch Cancel",eventName:"touchcancel"}),se({method:e,eventType:"Transition Start",eventNames:["transitionstart","webkitTransitionStart","MSTransitionStart","oTransitionStart","otransitionstart"]}),se({method:e,eventType:"Transition Iteration",eventNames:["transitioniteration","webkitTransitionIteration","MSTransitionIteration","oTransitionIteration","otransitioniteration"]}),se({method:e,eventType:"Transition End",eventNames:["transitionend","webkitTransitionEnd","MSTransitionEnd","oTransitionEnd","otransitionend"]}),"child"===A&&se({method:e,eventType:"IFrame Resized",eventName:"resize"})}function ue(e,t,n,i){return t!==e&&(e in n||(oe(e+" is not a valid option for "+i+"CalculationMethod."),e=t),ie(i+' calculation method set to "'+e+'"')),e}function le(){k=ue(k,b,Q,"height")}function fe(){J=ue(J,H,G,"width")}function de(){var e;!0===u?(ce("add"),e=0>E,window.MutationObserver||window.WebKitMutationObserver?e?he():m=function(){function e(e){function t(e){!1===e.complete&&(ie("Attach listeners to "+e.src),e.addEventListener("load",r,!1),e.addEventListener("error",a,!1),u.push(e))}"attributes"===e.type&&"src"===e.attributeName?t(e.target):"childList"===e.type&&Array.prototype.forEach.call(e.target.querySelectorAll("img"),t)}function n(e){u.splice(u.indexOf(e),1)}function i(e){ie("Remove listeners from "+e.src),e.removeEventListener("load",r,!1),e.removeEventListener("error",a,!1),n(e)}function o(e,n,o){i(e.target),be(n,o+": "+e.target.src,t,t)}function r(e){o(e,"imageLoad","Image loaded")}function a(e){o(e,"imageLoadFailed","Image load failed")}function s(t){be("mutationObserver","mutationObserver: "+t[0].target+" "+t[0].type),t.forEach(e)}function c(){var e=document.querySelector("body"),t={attributes:!0,attributeOldValue:!1,characterData:!0,characterDataOldValue:!1,childList:!0,subtree:!0};return f=new l(s),ie("Create body MutationObserver"),f.observe(e,t),f}var u=[],l=window.MutationObserver||window.WebKitMutationObserver,f=c();return{disconnect:function(){"disconnect"in f&&(ie("Disconnect body MutationObserver"),f.disconnect(),u.forEach(i))}}}():(ie("MutationObserver not supported in this browser!"),he())):ie("Auto Resize disabled")}function me(){ce("remove"),null!==m&&m.disconnect(),clearInterval(M)}function he(){0!==E&&(ie("setInterval: "+E+"ms"),M=setInterval((function(){be("interval","setInterval: "+E)}),Math.abs(E)))}function ge(e,t){var n=0;return t=t||document.body,n="defaultView"in document&&"getComputedStyle"in document.defaultView?null!==(n=document.defaultView.getComputedStyle(t,null))?n[e]:0:function(e){if(/^\d+(px)?$/i.test(e))return parseInt(e,10);var n=t.style.left,i=t.runtimeStyle.left;return t.runtimeStyle.left=t.currentStyle.left,t.style.left=e||0,e=t.style.pixelLeft,t.style.left=n,t.runtimeStyle.left=i,e}(t.currentStyle[e]),parseInt(n,10)}function pe(e,t){for(var n=t.length,i=0,o=0,r=te(e),a=K(),s=0;so&&(o=i);return a=K()-a,ie("Parsed "+n+" HTML elements"),ie("Element position calculated in "+a+"ms"),function(e){e>L/2&&ie("Event throttle increased to "+(L=2*e)+"ms")}(a),o}function ve(e){return[e.bodyOffset(),e.bodyScroll(),e.documentElementOffset(),e.documentElementScroll()]}function we(e,t){var n=document.querySelectorAll("["+t+"]");return 0===n.length&&(oe("No tagged elements ("+t+") found on page"),document.querySelectorAll("body *")),pe(e,n)}function ye(){return document.querySelectorAll("body *")}function be(e,t,n,i){j&&e in p?ie("Trigger event cancelled: "+e):(e in{reset:1,resetPage:1,init:1}||ie("Trigger event: "+t),Z(e,t,n,i))}function ke(){j||(j=!0,ie("Trigger event lock on")),clearTimeout(W),W=setTimeout((function(){j=!1,ie("Trigger event lock off"),ie("--")}),v)}function Oe(e){y=Q[k](),q=G[J](),xe(y,q,e)}function _e(e){var t=k;k=b,ie("Reset trigger event: "+e),ke(),Oe("reset"),k=t}function xe(e,n,i,o,r){var a;!0===R&&(t===r?r=z:ie("Message targetOrigin: "+r),ie("Sending message to host page ("+(a=N+":"+e+":"+n+":"+i+(t!==o?":"+o:""))+")"),F.postMessage(S+a,r))}function Ee(t){var n={init:function(){"interactive"===document.readyState||"complete"===document.readyState?(_=t.data,F=t.source,re(),w=!1,setTimeout((function(){O=!1}),v)):(ie("Waiting for page ready"),$(window,"readystatechange",n.initFromParent))},reset:function(){O?ie("Page reset ignored by init"):(ie("Page size reset by host page"),Oe("resetPage"))},resize:function(){be("resizeParent","Parent window requested size check")},moveToAnchor:function(){x.findTarget(o())},inPageLink:function(){this.moveToAnchor()},pageInfo:function(){var e=o();ie("PageInfoFromParent called from parent: "+e),U(JSON.parse(e)),ie(" --")},message:function(){var e=o();ie("MessageCallback called from parent: "+e),D(JSON.parse(e)),ie(" --")}};function i(){return t.data.split("]")[1].split(":")[0]}function o(){return t.data.substr(t.data.indexOf(":")+1)}function r(){return t.data.split(":")[2]in{true:1,false:1}}function a(){var o=i();o in n?n[o]():!e.exports&&"iFrameResize"in window||r()||oe("Unexpected message ("+t.data+")")}S===(""+t.data).substr(0,I)&&(!1===w?a():r()?n.init():ie('Ignored message of type "'+i()+'". Received before initialization.'))}}()},28:function(e,t){var n,i,o;!function(r){"use strict";if("undefined"!=typeof window){var a,s=0,c=!1,u=!1,l="message".length,f="[iFrameSizer]",d=f.length,m=null,h=window.requestAnimationFrame,g={max:1,scroll:1,bodyScroll:1,documentElementScroll:1},p={},v=null,w={autoResize:!0,bodyBackground:null,bodyMargin:null,bodyMarginV1:8,bodyPadding:null,checkOrigin:!0,inPageLinks:!1,enablePublicMethods:!0,heightCalculationMethod:"bodyOffset",id:"iFrameResizer",interval:32,log:!1,maxHeight:1/0,maxWidth:1/0,minHeight:0,minWidth:0,resizeFrom:"parent",scrolling:!1,sizeHeight:!0,sizeWidth:!1,warningTimeout:5e3,tolerance:0,widthCalculationMethod:"scroll",closedCallback:function(){},initCallback:function(){},messageCallback:function(){E("MessageCallback function not defined")},resizedCallback:function(){},scrollCallback:function(){return!0}};window.jQuery&&((a=window.jQuery).fn?a.fn.iFrameResize||(a.fn.iFrameResize=function(e){return this.filter("iframe").each((function(t,n){W(n,e)})).end()}):x("","Unable to bind to jQuery, it is not fully loaded.")),i=[],(o="function"==typeof(n=function(){function e(e,n){n&&(function(){if(!n.tagName)throw new TypeError("Object is not a valid DOM element");if("IFRAME"!==n.tagName.toUpperCase())throw new TypeError("Expected 11 | 12 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/integration/addon-container.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | describe('AddonContainer', () => { 4 | let page; 5 | let addonFrame; 6 | 7 | const getSpyCall = async (eventName) => { 8 | const spyCallsHandle = await addonFrame.evaluateHandle(() => new Promise((resolve) => { 9 | setTimeout(() => { 10 | resolve(addon.emit.getCalls().map((c) => c.args)); 11 | }); 12 | })); 13 | 14 | const spyCalls = await spyCallsHandle.jsonValue(); 15 | return _.find(spyCalls, (c) => c[0] === eventName); 16 | }; 17 | 18 | before(async () => { 19 | page = await browser.newPage(); 20 | await page.goto(url); 21 | [, addonFrame] = await page.frames(); 22 | }); 23 | 24 | after(async () => { 25 | await page.close(); 26 | }); 27 | 28 | beforeEach(async () => { 29 | await addonFrame.evaluate(() => new Promise((resolve) => { 30 | sinon.spy(addon, 'emit'); 31 | resolve(); 32 | })); 33 | }); 34 | 35 | afterEach(async () => { 36 | await addonFrame.evaluate(() => new Promise((resolve) => { 37 | addon.emit.restore(); 38 | resolve(); 39 | })); 40 | }); 41 | 42 | describe('init', () => { 43 | it('should pass options to Addon', async () => { 44 | const optionsHandle = await addonFrame 45 | .evaluateHandle(() => Promise.resolve(window.addonOptions)); 46 | const options = await optionsHandle.jsonValue(); 47 | 48 | expect(options).to.deep.equal({ test: 'test' }); 49 | }); 50 | }); 51 | 52 | describe('.trigger(eventName, eventData)', () => { 53 | it('should pass the event to Addon', async () => { 54 | const eventName = 'test event'; 55 | const eventData = { test: 'data' }; 56 | page.evaluate((eventName, eventData) => { 57 | container.trigger(eventName, eventData); 58 | }, eventName, eventData); 59 | const call = await getSpyCall(eventName); 60 | 61 | expect(call).to.exist; 62 | expect(call[1]).to.deep.equal(eventData); 63 | }); 64 | }); 65 | 66 | describe('.update(data)', () => { 67 | it('should trigger `update` event in Addon', async () => { 68 | const data = { test: 'test' }; 69 | page.evaluate((data) => { 70 | container.update(data); 71 | }, data); 72 | const call = await getSpyCall('update'); 73 | expect(call).to.exist; 74 | expect(call[1]).to.deep.equal(data); 75 | }); 76 | }); 77 | 78 | describe('.reload()', () => { 79 | it('should trigger `reload` event in Addon', async () => { 80 | page.evaluate(() => { 81 | container.reload(); 82 | }); 83 | const call = await getSpyCall('reload'); 84 | 85 | expect(call).to.exist; 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /tests/integration/addon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Addon Container 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/integration/addon.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import _ from 'lodash'; 3 | 4 | describe('Addon', () => { 5 | let page; 6 | 7 | const getSpyCall = async (eventName) => { 8 | const spyCallsHandle = await page.evaluateHandle(() => new Promise((resolve) => { 9 | setTimeout(() => { 10 | resolve(container.emit.getCalls().map((c) => c.args)); 11 | }); 12 | })); 13 | 14 | const spyCalls = await spyCallsHandle.jsonValue(); 15 | return _.find(spyCalls, (c) => c[0] === eventName); 16 | }; 17 | 18 | before(async () => { 19 | page = await browser.newPage(); 20 | await page.goto(url); 21 | }); 22 | 23 | after(async () => { 24 | await page.close(); 25 | }); 26 | 27 | beforeEach(async () => { 28 | await page.evaluate(() => new Promise((resolve) => { 29 | sinon.spy(container, 'emit'); 30 | resolve(); 31 | })); 32 | }); 33 | 34 | afterEach(async () => { 35 | await page.evaluate(() => new Promise((resolve) => { 36 | container.emit.restore(); 37 | resolve(); 38 | })); 39 | }); 40 | 41 | describe('.request(params)', () => { 42 | beforeEach(async () => { 43 | await page.evaluate(() => new Promise((resolve) => { 44 | container.on('request', (params, callback) => { 45 | if (params.query && params.query.shouldSuccess) { 46 | callback(null, { success: true }); 47 | } else { 48 | callback('error'); 49 | } 50 | }); 51 | 52 | resolve(); 53 | })); 54 | }); 55 | 56 | it('should receive success result from AddonContainer', async () => { 57 | const addonFrame = (await page.frames())[1]; 58 | const params = { method: 'GET', endpoint: 'test', query: { shouldSuccess: true } }; 59 | const result = await addonFrame.evaluate((params) => new Promise((resolve) => { 60 | addon.request(params).then((result) => { 61 | resolve(result); 62 | }).catch((err) => { 63 | resolve(err); 64 | }); 65 | }), params); 66 | const call = await getSpyCall('request'); 67 | 68 | expect(call).to.exist; 69 | expect(call[1]).to.deep.equal(params); 70 | expect(result).to.deep.equal({ success: true }); 71 | }); 72 | 73 | it('should receive error result from AddonContainer', async () => { 74 | const addonFrame = (await page.frames())[1]; 75 | const params = { method: 'GET', endpoint: 'test', query: { some: 'thing' } }; 76 | const result = await addonFrame.evaluate((params) => new Promise((resolve) => { 77 | addon.request(params).then((result) => { 78 | resolve(result); 79 | }).catch((err) => { 80 | resolve(err); 81 | }); 82 | }), params); 83 | const call = await getSpyCall('request'); 84 | 85 | expect(call).to.exist; 86 | expect(call[1]).to.deep.equal(params); 87 | expect(result).to.equal('error'); 88 | }); 89 | }); 90 | 91 | describe('.saveData(data)', () => { 92 | beforeEach(async () => { 93 | await page.evaluate(() => new Promise((resolve) => { 94 | container.on('saveData', (data, callback) => { 95 | if (data.shouldSave) { 96 | callback(); 97 | } else { 98 | callback('not saved'); 99 | } 100 | }); 101 | 102 | resolve(); 103 | })); 104 | }); 105 | 106 | it('should receive success result from AddonContainer', async () => { 107 | const addonFrame = (await page.frames())[1]; 108 | const data = { shouldSave: true }; 109 | 110 | const result = await addonFrame.evaluate((data) => new Promise((resolve) => { 111 | addon.saveData(data).then(() => { 112 | resolve({ saved: true }); 113 | }).catch((err) => { 114 | resolve(err); 115 | }); 116 | }), data); 117 | const call = await getSpyCall('saveData'); 118 | 119 | expect(call).to.exist; 120 | expect(call[1]).to.deep.equal(data); 121 | expect(result).to.deep.equal({ saved: true }); 122 | }); 123 | 124 | it('should receive error result from AddonContainer', async () => { 125 | const addonFrame = (await page.frames())[1]; 126 | const data = { shouldSave: false }; 127 | 128 | const result = await addonFrame.evaluate((data) => new Promise((resolve) => { 129 | addon.saveData(data).then(() => { 130 | resolve({ saved: true }); 131 | }).catch((err) => { 132 | resolve(err); 133 | }); 134 | }), data); 135 | const call = await getSpyCall('saveData'); 136 | 137 | expect(call).to.exist; 138 | expect(call[1]).to.deep.equal(data); 139 | expect(result).to.deep.equal('not saved'); 140 | }); 141 | }); 142 | 143 | ['addTransaction', 'addInstitution'].forEach((method) => { 144 | describe(`.${method}(attrs)`, () => { 145 | beforeEach(async () => { 146 | await page.evaluate((method) => new Promise((resolve) => { 147 | container.on(method, (attrs, callback) => { 148 | if (attrs.id === 'shouldCreate') { 149 | callback(null, { created: true }); 150 | } else if (attrs.id === 'shouldClose') { 151 | callback(null); 152 | } else { 153 | callback('error'); 154 | } 155 | }); 156 | 157 | resolve(); 158 | }), method); 159 | }); 160 | 161 | afterEach(async () => { 162 | await page.evaluate((method) => new Promise((resolve) => { 163 | container.off(method); 164 | 165 | resolve(); 166 | }), method); 167 | }); 168 | 169 | it('should receive success result with new item from AddonContainer', async () => { 170 | const addonFrame = (await page.frames())[1]; 171 | const attrs = { id: 'shouldCreate' }; 172 | 173 | const result = await addonFrame.evaluate(({ attrs, method }) => new Promise((resolve) => { 174 | addon[method](attrs).then((item) => { 175 | resolve(item); 176 | }).catch((err) => { 177 | resolve(err); 178 | }); 179 | }), { attrs, method }); 180 | const call = await getSpyCall(method); 181 | 182 | expect(call).to.exist; 183 | expect(call[1]).to.deep.equal(attrs); 184 | expect(result).to.deep.equal({ created: true }); 185 | }); 186 | 187 | it('should receive success result without new item from AddonContainer', async () => { 188 | const addonFrame = (await page.frames())[1]; 189 | const attrs = { id: 'shouldClose' }; 190 | 191 | const result = await addonFrame.evaluate(({ attrs, method }) => new Promise((resolve) => { 192 | addon[method](attrs).then((item) => { 193 | resolve(item); 194 | }).catch((err) => { 195 | resolve(err); 196 | }); 197 | }), { attrs, method }); 198 | const call = await getSpyCall(method); 199 | 200 | expect(call).to.exist; 201 | expect(call[1]).to.deep.equal(attrs); 202 | expect(result).to.not.exist; 203 | }); 204 | 205 | it('should receive error result from AddonContainer', async () => { 206 | const addonFrame = (await page.frames())[1]; 207 | const attrs = { id: 'error' }; 208 | 209 | const result = await addonFrame.evaluate(({ attrs, method }) => new Promise((resolve) => { 210 | addon[method](attrs).then((item) => { 211 | resolve(item); 212 | }).catch((err) => { 213 | resolve(err); 214 | }); 215 | }), { attrs, method }); 216 | const call = await getSpyCall(method); 217 | 218 | expect(call).to.exist; 219 | expect(call[1]).to.deep.equal(attrs); 220 | expect(result).to.equal('error'); 221 | }); 222 | }); 223 | }); 224 | 225 | [ 226 | 'editTransaction', 227 | 'editInstitution', 'editAsset', 'editLiability', 228 | 'deleteInstitution', 'deleteAsset', 'deleteLiability', 229 | ].forEach((method) => { 230 | describe(`.${method}(id)`, () => { 231 | beforeEach(async () => { 232 | await page.evaluate((method) => new Promise((resolve) => { 233 | container.on(method, (id, callback) => { 234 | if (id === 'shouldUpdate') { 235 | callback(null, { updated: true }); 236 | } else if (id === 'shouldClose') { 237 | callback(null); 238 | } else { 239 | callback('error'); 240 | } 241 | }); 242 | 243 | resolve(); 244 | }), method); 245 | }); 246 | 247 | afterEach(async () => { 248 | await page.evaluate((method) => new Promise((resolve) => { 249 | container.off(method); 250 | 251 | resolve(); 252 | }), method); 253 | }); 254 | 255 | it('should receive success result with updated data from AddonContainer', async () => { 256 | const addonFrame = (await page.frames())[1]; 257 | const id = 'shouldUpdate'; 258 | 259 | const result = await addonFrame.evaluate(({ id, method }) => new Promise((resolve) => { 260 | addon[method](id).then((updated) => { 261 | if (updated) resolve(updated); 262 | else resolve({ updated: false }); 263 | }).catch((err) => { 264 | resolve(err); 265 | }); 266 | }), { id, method }); 267 | const call = await getSpyCall(method); 268 | 269 | expect(call).to.exist; 270 | expect(call[1]).to.deep.equal(id); 271 | expect(result).to.deep.equal({ updated: true }); 272 | }); 273 | 274 | it('should receive success result without updated data from AddonContainer', async () => { 275 | const addonFrame = (await page.frames())[1]; 276 | const id = 'shouldClose'; 277 | 278 | const result = await addonFrame.evaluate(({ id, method }) => new Promise((resolve) => { 279 | addon[method](id).then((updated) => { 280 | if (updated) resolve(updated); 281 | else resolve({ updated: false }); 282 | }).catch((err) => { 283 | resolve(err); 284 | }); 285 | }), { id, method }); 286 | const call = await getSpyCall(method); 287 | 288 | expect(call).to.exist; 289 | expect(call[1]).to.deep.equal(id); 290 | expect(result).to.deep.equal({ updated: false }); 291 | }); 292 | 293 | it('should receive error result from AddonContainer', async () => { 294 | const addonFrame = (await page.frames())[1]; 295 | const id = 'test'; 296 | 297 | const result = await addonFrame.evaluate(({ id, method }) => new Promise((resolve) => { 298 | addon[method](id).then((updated) => { 299 | if (updated) resolve(updated); 300 | else resolve({ updated: false }); 301 | }).catch((err) => { 302 | resolve(err); 303 | }); 304 | }), { id, method }); 305 | const call = await getSpyCall(method); 306 | 307 | expect(call).to.exist; 308 | expect(call[1]).to.deep.equal(id); 309 | expect(result).to.equal('error'); 310 | }); 311 | }); 312 | }); 313 | 314 | describe('.addInvestment()', () => { 315 | afterEach(async () => { 316 | await page.evaluate(() => new Promise((resolve) => { 317 | container.off('addInvestment'); 318 | 319 | resolve(); 320 | })); 321 | }); 322 | 323 | describe('when container returns result', () => { 324 | beforeEach(async () => { 325 | await page.evaluate(() => new Promise((resolve) => { 326 | container.on('addInvestment', (callback) => { 327 | callback(null, { created: true }); 328 | }); 329 | 330 | resolve(); 331 | })); 332 | }); 333 | 334 | it('should receive result from AddonContainer', async () => { 335 | const addonFrame = (await page.frames())[1]; 336 | 337 | const result = await addonFrame.evaluate(() => new Promise((resolve) => { 338 | addon.addInvestment().then((result) => { 339 | resolve(result); 340 | }).catch((err) => { 341 | resolve(err); 342 | }); 343 | })); 344 | const call = await getSpyCall('addInvestment'); 345 | 346 | expect(call).to.exist; 347 | expect(result).to.deep.equal({ created: true }); 348 | }); 349 | }); 350 | 351 | describe('when container returns neither error nor result', () => { 352 | beforeEach(async () => { 353 | await page.evaluate(() => new Promise((resolve) => { 354 | container.on('addInvestment', (callback) => { 355 | callback(); 356 | }); 357 | 358 | resolve(); 359 | })); 360 | }); 361 | 362 | it('should proceed as a success', async () => { 363 | const addonFrame = (await page.frames())[1]; 364 | 365 | const result = await addonFrame.evaluate(() => new Promise((resolve) => { 366 | addon.addInvestment().then((result) => { 367 | resolve(result); 368 | }).catch((err) => { 369 | resolve(err); 370 | }); 371 | })); 372 | const call = await getSpyCall('addInvestment'); 373 | 374 | expect(call).to.exist; 375 | expect(result).to.not.exist; 376 | }); 377 | }); 378 | 379 | describe('when container returns an error', () => { 380 | beforeEach(async () => { 381 | await page.evaluate(() => new Promise((resolve) => { 382 | container.on('addInvestment', (callback) => { 383 | callback('error'); 384 | }); 385 | 386 | resolve(); 387 | })); 388 | }); 389 | 390 | it('should receive the error', async () => { 391 | const addonFrame = (await page.frames())[1]; 392 | 393 | const result = await addonFrame.evaluate(() => new Promise((resolve) => { 394 | addon.addInvestment().then((result) => { 395 | resolve(result); 396 | }).catch((err) => { 397 | resolve(err); 398 | }); 399 | })); 400 | const call = await getSpyCall('addInvestment'); 401 | 402 | expect(call).to.exist; 403 | expect(result).to.equal('error'); 404 | }); 405 | }); 406 | }); 407 | 408 | describe('.downloadDocument(id)', () => { 409 | beforeEach(async () => { 410 | await page.evaluate(() => new Promise((resolve) => { 411 | container.on('downloadDocument', (id, callback) => { 412 | if (id === 'shouldPass') { 413 | callback(); 414 | } else { 415 | callback('error'); 416 | } 417 | }); 418 | 419 | resolve(); 420 | })); 421 | }); 422 | 423 | afterEach(async () => { 424 | await page.evaluate(() => new Promise((resolve) => { 425 | container.off('downloadDocument'); 426 | 427 | resolve(); 428 | })); 429 | }); 430 | 431 | it('should receive success result from AddonContainer', async () => { 432 | const addonFrame = (await page.frames())[1]; 433 | const id = 'shouldPass'; 434 | 435 | const result = await addonFrame.evaluate((id) => new Promise((resolve) => { 436 | addon.downloadDocument(id).then(resolve).catch(resolve); 437 | }), id); 438 | const call = await getSpyCall('downloadDocument'); 439 | 440 | expect(call).to.exist; 441 | expect(call[1]).to.deep.equal(id); 442 | expect(result).to.not.exist; 443 | }); 444 | 445 | it('should receive error result from AddonContainer', async () => { 446 | const addonFrame = (await page.frames())[1]; 447 | const id = 'test'; 448 | 449 | const result = await addonFrame.evaluate((id) => new Promise((resolve) => { 450 | addon.downloadDocument(id).then(resolve).catch(resolve); 451 | }), id); 452 | const call = await getSpyCall('downloadDocument'); 453 | 454 | expect(call).to.exist; 455 | expect(call[1]).to.deep.equal(id); 456 | expect(result).to.equal('error'); 457 | }); 458 | }); 459 | 460 | describe('.upgradePremium()', () => { 461 | beforeEach(async () => { 462 | await page.evaluate(() => new Promise((resolve) => { 463 | container.on('upgradePremium', (callback) => { 464 | callback(); 465 | }); 466 | 467 | resolve(); 468 | })); 469 | }); 470 | 471 | afterEach(async () => { 472 | await page.evaluate(() => new Promise((resolve) => { 473 | container.off('upgradePremium'); 474 | 475 | resolve(); 476 | })); 477 | }); 478 | 479 | it('should be successful', async () => { 480 | const addonFrame = (await page.frames())[1]; 481 | 482 | const result = await addonFrame.evaluate(() => new Promise((resolve, reject) => { 483 | addon.upgradePremium().then(resolve).catch(reject); 484 | })); 485 | const call = await getSpyCall('upgradePremium'); 486 | 487 | expect(call).to.exist; 488 | expect(result).to.not.exist; 489 | }); 490 | }); 491 | 492 | describe('.getSharings()', () => { 493 | const sharings = [{ id: 'test1' }, { id: 'test2' }]; 494 | 495 | afterEach(async () => { 496 | await page.evaluate(() => new Promise((resolve) => { 497 | container.off('getSharings'); 498 | 499 | resolve(); 500 | })); 501 | }); 502 | 503 | describe('when container returns result', () => { 504 | beforeEach(async () => { 505 | await page.evaluate((sharings) => new Promise((resolve) => { 506 | container.on('getSharings', (callback) => { 507 | callback(null, sharings); 508 | }); 509 | 510 | resolve(); 511 | }), sharings); 512 | }); 513 | 514 | it('should receive the result', async () => { 515 | const addonFrame = (await page.frames())[1]; 516 | 517 | const result = await addonFrame.evaluate(() => new Promise((resolve) => { 518 | addon.getSharings().then((sharings) => { 519 | resolve(sharings); 520 | }).catch((err) => { 521 | resolve(err); 522 | }); 523 | })); 524 | const call = await getSpyCall('getSharings'); 525 | 526 | expect(call).to.exist; 527 | expect(result).to.deep.equal(sharings); 528 | }); 529 | }); 530 | 531 | describe('when container returns error', () => { 532 | beforeEach(async () => { 533 | await page.evaluate(() => new Promise((resolve) => { 534 | container.on('getSharings', (callback) => { 535 | callback('error'); 536 | }); 537 | 538 | resolve(); 539 | })); 540 | }); 541 | 542 | it('should receive the error', async () => { 543 | const addonFrame = (await page.frames())[1]; 544 | 545 | const result = await addonFrame.evaluate(() => new Promise((resolve) => { 546 | addon.getSharings().then((sharings) => { 547 | resolve(sharings); 548 | }).catch((err) => { 549 | resolve(err); 550 | }); 551 | })); 552 | const call = await getSpyCall('getSharings'); 553 | 554 | expect(call).to.exist; 555 | expect(result).to.equal('error'); 556 | }); 557 | }); 558 | }); 559 | 560 | describe('.switchUser(id)', () => { 561 | beforeEach(async () => { 562 | await page.evaluate(() => new Promise((resolve) => { 563 | container.on('switchUser', (id, callback) => { 564 | if (id === 'shouldPass') { 565 | callback(); 566 | } else { 567 | callback('error'); 568 | } 569 | }); 570 | 571 | resolve(); 572 | })); 573 | }); 574 | 575 | afterEach(async () => { 576 | await page.evaluate(() => new Promise((resolve) => { 577 | container.off('switchUser'); 578 | 579 | resolve(); 580 | })); 581 | }); 582 | 583 | it('should receive success result from AddonContainer', async () => { 584 | const addonFrame = (await page.frames())[1]; 585 | const id = 'shouldPass'; 586 | 587 | const result = await addonFrame.evaluate((id) => new Promise((resolve) => { 588 | addon.switchUser(id).then(resolve).catch(resolve); 589 | }), id); 590 | const call = await getSpyCall('switchUser'); 591 | 592 | expect(call).to.exist; 593 | expect(call[1]).to.deep.equal(id); 594 | expect(result).to.not.exist; 595 | }); 596 | 597 | it('should receive error result from AddonContainer', async () => { 598 | const addonFrame = (await page.frames())[1]; 599 | const id = 'test'; 600 | 601 | const result = await addonFrame.evaluate((id) => new Promise((resolve) => { 602 | addon.switchUser(id).then(resolve).catch(resolve); 603 | }), id); 604 | const call = await getSpyCall('switchUser'); 605 | 606 | expect(call).to.exist; 607 | expect(call[1]).to.deep.equal(id); 608 | expect(result).to.equal('error'); 609 | }); 610 | }); 611 | }); 612 | -------------------------------------------------------------------------------- /tests/integration/bootstrap.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import { expect } from 'chai'; 3 | import { fork } from 'child_process'; 4 | import _ from 'lodash'; 5 | 6 | const globalVariables = _.pick(global, ['browser', 'expect', 'server', 'url']); 7 | 8 | // puppeteer options 9 | const opts = { 10 | headless: true, 11 | slowMo: 100, 12 | timeout: 10000, 13 | args: ['--no-sandbox', '--disable-setuid-sandbox'], 14 | }; 15 | 16 | // expose variables 17 | before(async () => { 18 | global.expect = expect; 19 | global.server = fork('node_modules/.bin/serve', ['.', '-p', '9898'], { 20 | stdio: 'ignore', 21 | }); 22 | global.server.unref(); 23 | global.url = 'http://localhost:9898/tests/integration/addon-container.html'; 24 | global.browser = await puppeteer.launch(opts); 25 | }); 26 | 27 | // close browser and reset global variables 28 | after(async () => { 29 | server.kill('SIGTERM'); 30 | await browser.close(); 31 | 32 | global.browser = globalVariables.browser; 33 | global.expect = globalVariables.expect; 34 | global.server = globalVariables.server; 35 | global.url = globalVariables.url; 36 | }); 37 | -------------------------------------------------------------------------------- /tests/unit/addon-container.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | 4 | import sinon from 'sinon'; 5 | import AddonContainer from '../../src/addon-container'; 6 | 7 | chai.use(chaiAsPromised); 8 | const { expect } = chai; 9 | 10 | describe('AddonContainer', () => { 11 | let container; 12 | 13 | before(() => { 14 | // JsChannel requires JSON implementation. 15 | window.JSON = { 16 | stringify: () => {}, 17 | parse: () => {}, 18 | }; 19 | 20 | const iframe = document.createElement('iframe'); 21 | document.body.appendChild(iframe); 22 | iframe.src = 'about:blank'; 23 | container = new AddonContainer({ iframe }); 24 | sinon.spy(container.channel, 'call'); 25 | sinon.spy(container.channel, 'destroy'); 26 | }); 27 | 28 | after(() => { 29 | if (container) { 30 | container.destroy(); 31 | container = undefined; 32 | } 33 | }); 34 | 35 | it('should setup js-channel channel', () => { 36 | expect(container.channel).to.be.an('object'); 37 | expect(container.channel).to.have.all.keys('bind', 'unbind', 'notify', 'call', 'destroy'); 38 | }); 39 | 40 | describe('.trigger(eventName, eventData)', () => { 41 | it('should pass the event via _event through the channel', () => { 42 | const eventName = 'test event'; 43 | const eventData = 'test data'; 44 | container.trigger(eventName, eventData); 45 | const spyCall = container.channel.call.lastCall; 46 | const calledArgs = spyCall.args[0]; 47 | 48 | expect(calledArgs.method).to.equal('_event'); 49 | expect(calledArgs.params).to.deep.equal({ 50 | eventName: 'test event', 51 | eventData: 'test data', 52 | }); 53 | }); 54 | 55 | it('should not pass undefined eventData', () => { 56 | const eventName = 'test event'; 57 | container.trigger(eventName); 58 | const spyCall = container.channel.call.lastCall; 59 | const calledArgs = spyCall.args[0]; 60 | 61 | expect(calledArgs.method).to.equal('_event'); 62 | expect(calledArgs.params).to.deep.equal({ eventName: 'test event' }); 63 | }); 64 | }); 65 | 66 | describe('.update(data)', () => { 67 | it('should send the updated data through the channel', () => { 68 | const data = { test: 'test' }; 69 | container.update(data); 70 | const spyCall = container.channel.call.lastCall; 71 | const calledArgs = spyCall.args[0]; 72 | 73 | expect(calledArgs.method).to.equal('update'); 74 | expect(calledArgs.params).to.deep.equal({ test: 'test' }); 75 | }); 76 | 77 | it('should raise an error if data is not an object', () => { 78 | const errorMessage = 'Data must be an object'; 79 | const numCalls = container.channel.call.getCalls().length; 80 | 81 | ['string', 1, true, false, undefined, null].forEach((params) => { 82 | expect(container.update(params)).to.eventually.be.rejectedWith(errorMessage); 83 | }); 84 | 85 | expect(container.channel.call.getCalls().length).to.equal(numCalls); 86 | }); 87 | }); 88 | 89 | describe('.reload()', () => { 90 | it('should call reload on the channel', () => { 91 | container.reload(); 92 | const spyCall = container.channel.call.lastCall; 93 | const calledArgs = spyCall.args[0]; 94 | 95 | expect(calledArgs.method).to.equal('reload'); 96 | }); 97 | }); 98 | 99 | describe('.destroy()', () => { 100 | it("should call channel's destroy", () => { 101 | container.destroy(); 102 | const spyCall = container.channel.destroy.lastCall; 103 | container = undefined; 104 | 105 | expect(spyCall).to.exist; 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /tests/unit/addon.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | 4 | import sinon from 'sinon'; 5 | import { JSDOM } from 'jsdom'; 6 | import Addon from '../../src/addon'; 7 | 8 | chai.use(chaiAsPromised); 9 | const { expect } = chai; 10 | 11 | describe('Addon', () => { 12 | let addon; 13 | 14 | before(() => { 15 | // JsChannel requires JSON implementation while JSDOM does not provide one. 16 | window.JSON = { 17 | stringify: () => {}, 18 | parse: () => {}, 19 | }; 20 | 21 | addon = new Addon({ window: new JSDOM().window }); 22 | sinon.spy(addon.channel, 'call'); 23 | sinon.spy(addon.channel, 'destroy'); 24 | }); 25 | 26 | after(() => { 27 | if (addon) { 28 | addon.destroy(); 29 | addon = undefined; 30 | } 31 | }); 32 | 33 | it('should configure heightCalculationMethod for iFrameResizer', () => { 34 | expect(window.iFrameResizer.heightCalculationMethod).to.exist; 35 | }); 36 | 37 | it('should setup js-channel channel', () => { 38 | expect(addon.channel).to.be.an('object'); 39 | expect(addon.channel).to.have.all.keys('bind', 'unbind', 'notify', 'call', 'destroy'); 40 | }); 41 | 42 | describe('.request(params)', () => { 43 | it("should call channel's `request` method with the right params", () => { 44 | const params = { 45 | method: 'GET', endpoint: 'test', query: { some: 'thing' }, body: { another: 'thing' }, 46 | }; 47 | addon.request(params); 48 | const spyCall = addon.channel.call.lastCall; 49 | const calledArgs = spyCall.args[0]; 50 | 51 | expect(calledArgs.method).to.equal('request'); 52 | expect(calledArgs.params).to.deep.equal(params); 53 | }); 54 | 55 | it('should raise an error if params is not an object', () => { 56 | const errorMessage = 'Params must be an object'; 57 | const numCalls = addon.channel.call.getCalls().length; 58 | 59 | ['string', 1, true, false, undefined, null, []].forEach((params) => { 60 | expect(addon.request(params)).to.eventually.be.rejectedWith(errorMessage); 61 | }); 62 | 63 | expect(addon.channel.call.getCalls().length).to.equal(numCalls); 64 | }); 65 | 66 | it('should raise an error if method or endpoint is missing or invalid', () => { 67 | const errorMessage = 'Invalid method or endpoint'; 68 | const numCalls = addon.channel.call.getCalls().length; 69 | 70 | [1, true, false, null, undefined, [], {}, ''].forEach((invalid) => { 71 | expect(addon.request({ method: invalid, endpoint: 'test' })) 72 | .to.eventually.be.rejectedWith(errorMessage); 73 | expect(addon.request({ method: 'test', endpoint: invalid })) 74 | .to.eventually.be.rejectedWith(errorMessage); 75 | }); 76 | 77 | expect(addon.channel.call.getCalls().length).to.equal(numCalls); 78 | }); 79 | 80 | it('should raise an error if query is not an object', () => { 81 | const errorMessage = 'Query must be an object'; 82 | const numCalls = addon.channel.call.getCalls().length; 83 | 84 | ['string', 1, true, false, null, []].forEach((query) => { 85 | expect(addon.request({ method: 'GET', endpoint: 'test', query })) 86 | .to.eventually.be.rejectedWith(errorMessage); 87 | }); 88 | 89 | expect(addon.channel.call.getCalls().length).to.equal(numCalls); 90 | }); 91 | 92 | it('should still proceed if query is not provided', () => { 93 | const validParams = { method: 'GET', endpoint: 'test', query: undefined }; 94 | addon.request(validParams); 95 | const spyCall = addon.channel.call.lastCall; 96 | const calledArgs = spyCall.args[0]; 97 | 98 | expect(calledArgs.method).to.equal('request'); 99 | expect(calledArgs.params).to.deep.equal(validParams); 100 | }); 101 | 102 | it('should raise an error if body is not an object', () => { 103 | const errorMessage = 'Body must be an object'; 104 | const numCalls = addon.channel.call.getCalls().length; 105 | 106 | ['string', 1, true, false, null, []].forEach((body) => { 107 | expect(addon.request({ method: 'GET', endpoint: 'test', body })) 108 | .to.eventually.be.rejectedWith(errorMessage); 109 | }); 110 | 111 | expect(addon.channel.call.getCalls().length).to.equal(numCalls); 112 | }); 113 | 114 | it('should still proceed if body is not provided', () => { 115 | const validParams = { method: 'GET', endpoint: 'test', body: undefined }; 116 | addon.request(validParams); 117 | const spyCall = addon.channel.call.lastCall; 118 | const calledArgs = spyCall.args[0]; 119 | 120 | expect(calledArgs.method).to.equal('request'); 121 | expect(calledArgs.params).to.deep.equal(validParams); 122 | }); 123 | 124 | it('should pass effectiveUser if set', () => { 125 | const params = { 126 | method: 'GET', endpoint: 'test', query: { some: 'thing' }, body: { another: 'thing' }, 127 | }; 128 | addon.setEffectiveUser('test'); 129 | addon.request(params); 130 | const spyCall = addon.channel.call.lastCall; 131 | const calledArgs = spyCall.args[0]; 132 | 133 | expect(calledArgs.method).to.equal('request'); 134 | expect(calledArgs.params).to.deep.equal({ ...params, effectiveUser: 'test' }); 135 | }); 136 | 137 | it('should not pass effectiveUser if not set or null', () => { 138 | const params = { 139 | method: 'GET', endpoint: 'test', query: { some: 'thing' }, body: { another: 'thing' }, 140 | }; 141 | addon.setEffectiveUser(null); 142 | addon.request(params); 143 | const spyCall = addon.channel.call.lastCall; 144 | const calledArgs = spyCall.args[0]; 145 | 146 | expect(calledArgs.method).to.equal('request'); 147 | expect(calledArgs.params).to.deep.equal(params); 148 | }); 149 | }); 150 | 151 | ['saveData', 'addTransaction', 'addInstitution'].forEach((method) => { 152 | describe(`.${method}(attrs)`, () => { 153 | it(`should call channel's \`${method}\` method with the attrs`, () => { 154 | const attrs = { test: 1 }; 155 | addon[method](attrs); 156 | const spyCall = addon.channel.call.lastCall; 157 | const calledArgs = spyCall.args[0]; 158 | 159 | expect(calledArgs.method).to.equal(method); 160 | expect(calledArgs.params).to.equal(attrs); 161 | }); 162 | 163 | it('should raise an error if attrs is invalid', () => { 164 | const errorMessage = 'Attrs must be an object'; 165 | const numCalls = addon.channel.call.getCalls().length; 166 | 167 | [1, true, false, null, [], ''].forEach((invalid) => { 168 | expect(addon[method](invalid)).to.eventually.be.rejectedWith(errorMessage); 169 | }); 170 | 171 | expect(addon.channel.call.getCalls().length).to.equal(numCalls); 172 | }); 173 | }); 174 | }); 175 | 176 | [ 177 | 'editTransaction', 178 | 'editInstitution', 'editAsset', 'editLiability', 179 | 'deleteInstitution', 'deleteAsset', 'deleteLiability', 180 | 'downloadDocument', 181 | 'switchUser', 182 | ].forEach((method) => { 183 | describe(`.${method}(id)`, () => { 184 | it(`should call channel's \`${method}\` method with the id`, () => { 185 | const id = 'test'; 186 | addon[method](id); 187 | const spyCall = addon.channel.call.lastCall; 188 | const calledArgs = spyCall.args[0]; 189 | 190 | expect(calledArgs.method).to.equal(method); 191 | expect(calledArgs.params).to.deep.equal(id); 192 | }); 193 | 194 | it('should raise an error if id is missing or invalid', () => { 195 | const errorMessage = 'Invalid id'; 196 | const numCalls = addon.channel.call.getCalls().length; 197 | 198 | [1, true, false, null, undefined, [], {}, ''].forEach((invalid) => { 199 | expect(addon[method](invalid)).to.eventually.be.rejectedWith(errorMessage); 200 | }); 201 | 202 | expect(addon.channel.call.getCalls().length).to.equal(numCalls); 203 | }); 204 | }); 205 | }); 206 | 207 | ['addInvestment', 'upgradePremium', 'getSharings', 'printPage'].forEach((method) => { 208 | describe(`.${method}()`, () => { 209 | it(`should call channel's \`${method}\` method`, () => { 210 | addon[method](); 211 | const spyCall = addon.channel.call.lastCall; 212 | const calledArgs = spyCall.args[0]; 213 | 214 | expect(calledArgs.method).to.equal(method); 215 | }); 216 | }); 217 | }); 218 | 219 | describe('.destroy()', () => { 220 | it("should call channel's destroy", () => { 221 | addon.destroy(); 222 | const spyCall = addon.channel.destroy.lastCall; 223 | addon = undefined; 224 | 225 | expect(spyCall).to.exist; 226 | }); 227 | }); 228 | }); 229 | -------------------------------------------------------------------------------- /tests/unit/api.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import { JSDOM } from 'jsdom'; 4 | import Addon from '../../src/addon'; 5 | 6 | describe('API', () => { 7 | let addon; 8 | 9 | before(() => { 10 | // JsChannel requires JSON implementation while JSDOM does not provide one. 11 | window.JSON = { 12 | stringify: () => {}, 13 | parse: () => {}, 14 | }; 15 | 16 | addon = new Addon({ window: new JSDOM().window }); 17 | sinon.spy(addon.channel, 'call'); 18 | }); 19 | 20 | after(() => { 21 | if (addon) { 22 | addon.destroy(); 23 | addon = undefined; 24 | } 25 | }); 26 | 27 | describe('.getAssets(query)', () => { 28 | it('should execute request', () => { 29 | const query = { some: 'thing' }; 30 | addon.api.getAssets(query); 31 | const spyCall = addon.channel.call.lastCall; 32 | const calledArgs = spyCall.args[0]; 33 | 34 | expect(calledArgs.method).to.equal('request'); 35 | expect(calledArgs.params.endpoint).to.equal('assets'); 36 | expect(calledArgs.params.query).to.equal(query); 37 | expect(calledArgs.params.method).to.equal('GET'); 38 | }); 39 | }); 40 | 41 | describe('.getCurrencies(query)', () => { 42 | it('should execute request', () => { 43 | const query = { some: 'thing' }; 44 | addon.api.getCurrencies(query); 45 | const spyCall = addon.channel.call.lastCall; 46 | const calledArgs = spyCall.args[0]; 47 | 48 | expect(calledArgs.method).to.equal('request'); 49 | expect(calledArgs.params.endpoint).to.equal('currencies'); 50 | expect(calledArgs.params.query).to.equal(query); 51 | expect(calledArgs.params.method).to.equal('GET'); 52 | }); 53 | }); 54 | 55 | describe('.getInstitutions(query)', () => { 56 | it('should execute request', () => { 57 | const query = { some: 'thing' }; 58 | addon.api.getInstitutions(query); 59 | const spyCall = addon.channel.call.lastCall; 60 | const calledArgs = spyCall.args[0]; 61 | 62 | expect(calledArgs.method).to.equal('request'); 63 | expect(calledArgs.params.endpoint).to.equal('institutions'); 64 | expect(calledArgs.params.query).to.equal(query); 65 | expect(calledArgs.params.method).to.equal('GET'); 66 | }); 67 | }); 68 | 69 | describe('.getInstitution(id)', () => { 70 | it('should execute request', () => { 71 | addon.api.getInstitution('test'); 72 | const spyCall = addon.channel.call.lastCall; 73 | const calledArgs = spyCall.args[0]; 74 | 75 | expect(calledArgs.method).to.equal('request'); 76 | expect(calledArgs.params.endpoint).to.equal('institutions/test'); 77 | expect(calledArgs.params.method).to.equal('GET'); 78 | }); 79 | }); 80 | 81 | describe('.pollInstitution(id, v)', () => { 82 | it('should execute request', () => { 83 | addon.api.pollInstitution('test', 1); 84 | const spyCall = addon.channel.call.lastCall; 85 | const calledArgs = spyCall.args[0]; 86 | 87 | expect(calledArgs.method).to.equal('request'); 88 | expect(calledArgs.params.endpoint).to.equal('institutions/test/poll?v=1'); 89 | expect(calledArgs.params.method).to.equal('GET'); 90 | }); 91 | }); 92 | 93 | describe('.syncInstitution(id)', () => { 94 | it('should execute request', () => { 95 | addon.api.syncInstitution('test'); 96 | const spyCall = addon.channel.call.lastCall; 97 | const calledArgs = spyCall.args[0]; 98 | 99 | expect(calledArgs.method).to.equal('request'); 100 | expect(calledArgs.params.endpoint).to.equal('institutions/test/sync'); 101 | expect(calledArgs.params.method).to.equal('POST'); 102 | }); 103 | }); 104 | 105 | describe('.addInstitution(data)', () => { 106 | it('should execute request', () => { 107 | const data = { some: 'thing' }; 108 | addon.api.addInstitution(data); 109 | const spyCall = addon.channel.call.lastCall; 110 | const calledArgs = spyCall.args[0]; 111 | 112 | expect(calledArgs.method).to.equal('request'); 113 | expect(calledArgs.params.endpoint).to.equal('institutions'); 114 | expect(calledArgs.params.body).to.equal(data); 115 | expect(calledArgs.params.method).to.equal('POST'); 116 | }); 117 | }); 118 | 119 | describe('.getLiabilities(query)', () => { 120 | it('should execute request', () => { 121 | const query = { some: 'thing' }; 122 | addon.api.getLiabilities(query); 123 | const spyCall = addon.channel.call.lastCall; 124 | const calledArgs = spyCall.args[0]; 125 | 126 | expect(calledArgs.method).to.equal('request'); 127 | expect(calledArgs.params.endpoint).to.equal('liabilities'); 128 | expect(calledArgs.params.query).to.equal(query); 129 | }); 130 | }); 131 | 132 | describe('.getPositions(query)', () => { 133 | it('should execute request', () => { 134 | const query = { some: 'thing' }; 135 | addon.api.getPositions(query); 136 | const spyCall = addon.channel.call.lastCall; 137 | const calledArgs = spyCall.args[0]; 138 | 139 | expect(calledArgs.method).to.equal('request'); 140 | expect(calledArgs.params.endpoint).to.equal('positions'); 141 | expect(calledArgs.params.query).to.equal(query); 142 | }); 143 | }); 144 | 145 | describe('.getTransactions(query)', () => { 146 | it('should execute request', () => { 147 | const query = { some: 'thing' }; 148 | addon.api.getTransactions(query); 149 | const spyCall = addon.channel.call.lastCall; 150 | const calledArgs = spyCall.args[0]; 151 | 152 | expect(calledArgs.method).to.equal('request'); 153 | expect(calledArgs.params.endpoint).to.equal('transactions'); 154 | expect(calledArgs.params.query).to.equal(query); 155 | }); 156 | }); 157 | 158 | describe('.updateTransaction(id, attrs)', () => { 159 | it('should execute request', () => { 160 | const attrs = { some: 'thing' }; 161 | addon.api.updateTransaction('test', attrs); 162 | const spyCall = addon.channel.call.lastCall; 163 | const calledArgs = spyCall.args[0]; 164 | 165 | expect(calledArgs.method).to.equal('request'); 166 | expect(calledArgs.params.endpoint).to.equal('transactions/test'); 167 | expect(calledArgs.params.body).to.equal(attrs); 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import merge from 'webpack-merge'; 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | import TerserPlugin from 'terser-webpack-plugin'; 5 | import ESLintPlugin from 'eslint-webpack-plugin'; 6 | 7 | const include = join(__dirname, 'src'); 8 | 9 | const browserConfig = { 10 | entry: { 11 | Addon: { 12 | import: './src/addon', 13 | filename: 'addon.js', 14 | library: { 15 | name: '[name]', 16 | type: 'var', 17 | }, 18 | }, 19 | AddonContainer: { 20 | import: './src/addon-container', 21 | filename: 'addon-container.js', 22 | library: { 23 | name: '[name]', 24 | type: 'var', 25 | }, 26 | }, 27 | }, 28 | output: { 29 | path: join(__dirname, 'dist'), 30 | }, 31 | optimization: { 32 | minimize: false, 33 | minimizer: [ 34 | new TerserPlugin({ 35 | extractComments: false, 36 | }), 37 | ], 38 | }, 39 | mode: 'production', 40 | module: { 41 | rules: [ 42 | { test: /\.js$/, loader: 'babel-loader', include }, 43 | ], 44 | }, 45 | }; 46 | 47 | const browserMinifiedConfig = merge(browserConfig, { 48 | devtool: 'source-map', 49 | entry: { 50 | Addon: { 51 | filename: 'addon.min.js', 52 | }, 53 | AddonContainer: { 54 | filename: 'addon-container.min.js', 55 | }, 56 | }, 57 | optimization: { 58 | minimize: true, 59 | }, 60 | }); 61 | 62 | const browserES5Config = merge(browserConfig, { 63 | entry: { 64 | Addon: { 65 | filename: 'addon.es5.js', 66 | }, 67 | AddonContainer: { 68 | filename: 'addon-container.es5.js', 69 | }, 70 | }, 71 | target: ['web', 'es5'], 72 | }); 73 | 74 | const browserES5MinifiedConfig = merge(browserES5Config, { 75 | devtool: 'source-map', 76 | entry: { 77 | Addon: { 78 | filename: 'addon.es5.min.js', 79 | }, 80 | AddonContainer: { 81 | filename: 'addon-container.es5.min.js', 82 | }, 83 | }, 84 | optimization: { 85 | minimize: true, 86 | }, 87 | }); 88 | 89 | const commonJsConfig = merge(browserConfig, { 90 | entry: { 91 | Addon: { 92 | library: { 93 | type: 'commonjs', 94 | }, 95 | }, 96 | AddonContainer: { 97 | library: { 98 | type: 'commonjs', 99 | }, 100 | }, 101 | }, 102 | output: { 103 | path: join(__dirname, 'lib'), 104 | }, 105 | }); 106 | 107 | const commonJsES5Config = merge(commonJsConfig, { 108 | entry: { 109 | Addon: { 110 | filename: 'addon.es5.js', 111 | }, 112 | AddonContainer: { 113 | filename: 'addon-container.es5.js', 114 | }, 115 | }, 116 | plugins: [ 117 | // Have eslint here so it only run once instead of once for each build config 118 | new ESLintPlugin(), 119 | ], 120 | target: 'es5', 121 | }); 122 | 123 | module.exports = [ 124 | browserConfig, 125 | browserMinifiedConfig, 126 | browserES5Config, 127 | browserES5MinifiedConfig, 128 | commonJsConfig, 129 | commonJsES5Config, 130 | ]; 131 | --------------------------------------------------------------------------------