Repository: Nickersoft/push.js Branch: master Commit: f799843c9bc5 Files: 39 Total size: 90.8 KB Directory structure: gitextract_6ktotom7/ ├── .babelrc ├── .browserslistrc ├── .codeclimate.yml ├── .csslintrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .flowconfig ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bin/ │ └── push.js ├── bower.json ├── index.d.ts ├── package.json ├── rollup.config.js ├── src/ │ ├── agents/ │ │ ├── AbstractAgent.js │ │ ├── DesktopAgent.js │ │ ├── MSAgent.js │ │ ├── MobileChromeAgent.js │ │ ├── MobileFirefoxAgent.js │ │ ├── WebKitAgent.js │ │ └── index.js │ ├── index.js │ ├── push/ │ │ ├── Messages.js │ │ ├── Permission.js │ │ ├── Push.js │ │ ├── Util.js │ │ └── index.js │ ├── serviceWorker.js │ └── types.js └── tests/ ├── browsers.conf.js ├── karma.conf.js └── push.tests.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ [ "@babel/preset-env", { "modules": false } ], "@babel/preset-flow" ], "plugins": [ "@babel/plugin-transform-flow-strip-types", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-import-meta", "@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-json-strings", [ "@babel/plugin-proposal-decorators", { "legacy": true } ], "@babel/plugin-proposal-function-sent", "@babel/plugin-proposal-export-namespace-from", "@babel/plugin-proposal-numeric-separator", "@babel/plugin-proposal-throw-expressions" ] } ================================================ FILE: .browserslistrc ================================================ Firefox >= 22 Chrome >= 5 Safari >= 6 Opera >= 25 Android >= 4.4 Blackberry >= 10 OperaMobile >= 37 FirefoxAndroid >= 47 Samsung >= 4 Explorer >= 9 ================================================ FILE: .codeclimate.yml ================================================ --- engines: csslint: enabled: true duplication: enabled: true config: languages: - ruby - javascript - python - php eslint: enabled: true fixme: enabled: true ratings: paths: - "**.css" - "**.inc" - "**.js" - "**.jsx" - "**.module" - "**.php" - "**.py" - "**.rb" exclude_paths: - node_modules/ - tests/ - bin/ ================================================ FILE: .csslintrc ================================================ --exclude-exts=.min.css --ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] indent_style = space indent_size = 4 tab_width = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: .eslintignore ================================================ **/*{.,-}min.js ================================================ FILE: .eslintrc.yml ================================================ --- parserOptions: sourceType: module ecmaFeatures: jsx: true env: amd: true browser: true es6: true jquery: true node: true # http://eslint.org/docs/rules/ rules: # Possible Errors no-await-in-loop: off no-cond-assign: error no-console: off no-constant-condition: error no-control-regex: error no-debugger: error no-dupe-args: error no-dupe-keys: error no-duplicate-case: error no-empty-character-class: error no-empty: error no-ex-assign: error no-extra-boolean-cast: error no-extra-parens: off no-extra-semi: error no-func-assign: error no-inner-declarations: - error - functions no-invalid-regexp: error no-irregular-whitespace: error no-negated-in-lhs: error no-obj-calls: error no-prototype-builtins: off no-regex-spaces: error no-sparse-arrays: error no-template-curly-in-string: off no-unexpected-multiline: error no-unreachable: error no-unsafe-finally: off no-unsafe-negation: off use-isnan: error valid-jsdoc: off valid-typeof: error # Best Practices accessor-pairs: error array-callback-return: off block-scoped-var: off class-methods-use-this: off complexity: - error - 6 consistent-return: off curly: off default-case: off dot-location: off dot-notation: off eqeqeq: error guard-for-in: error no-alert: error no-caller: error no-case-declarations: error no-div-regex: error no-else-return: off no-empty-function: off no-empty-pattern: error no-eq-null: error no-eval: error no-extend-native: error no-extra-bind: error no-extra-label: off no-fallthrough: error no-floating-decimal: off no-global-assign: off no-implicit-coercion: off no-implied-eval: error no-invalid-this: off no-iterator: error no-labels: - error - allowLoop: true allowSwitch: true no-lone-blocks: error no-loop-func: error no-magic-number: off no-multi-spaces: off no-multi-str: off no-native-reassign: error no-new-func: error no-new-wrappers: error no-new: error no-octal-escape: error no-octal: error no-param-reassign: off no-proto: error no-redeclare: error no-restricted-properties: off no-return-assign: error no-return-await: off no-script-url: error no-self-assign: off no-self-compare: error no-sequences: off no-throw-literal: off no-unmodified-loop-condition: off no-unused-expressions: error no-unused-labels: off no-useless-call: error no-useless-concat: error no-useless-escape: off no-useless-return: off no-void: error no-warning-comments: off no-with: error prefer-promise-reject-errors: off radix: error require-await: off vars-on-top: off wrap-iife: error yoda: off # Strict strict: off # Variables init-declarations: off no-catch-shadow: error no-delete-var: error no-label-var: error no-restricted-globals: off no-shadow-restricted-names: error no-shadow: off no-undef-init: error no-undef: off no-undefined: off no-unused-vars: off no-use-before-define: off # Node.js and CommonJS callback-return: error global-require: error handle-callback-err: error no-mixed-requires: off no-new-require: off no-path-concat: error no-process-env: off no-process-exit: error no-restricted-modules: off no-sync: off # Stylistic Issues array-bracket-spacing: off block-spacing: off brace-style: off camelcase: off capitalized-comments: off comma-dangle: - error - never comma-spacing: off comma-style: off computed-property-spacing: off consistent-this: off eol-last: off func-call-spacing: off func-name-matching: off func-names: off func-style: off id-length: off id-match: off indent: off jsx-quotes: off key-spacing: off keyword-spacing: off line-comment-position: off linebreak-style: off lines-around-comment: off lines-around-directive: off max-depth: off max-len: off max-nested-callbacks: off max-params: off max-statements-per-line: off max-statements: - error - 30 multiline-ternary: off new-cap: off new-parens: off newline-after-var: off newline-before-return: off newline-per-chained-call: off no-array-constructor: off no-bitwise: off no-continue: off no-inline-comments: off no-lonely-if: off no-mixed-operators: off no-mixed-spaces-and-tabs: off no-multi-assign: off no-multiple-empty-lines: off no-negated-condition: off no-nested-ternary: off no-new-object: off no-plusplus: off no-restricted-syntax: off no-spaced-func: off no-tabs: off no-ternary: off no-trailing-spaces: off no-underscore-dangle: off no-unneeded-ternary: off object-curly-newline: off object-curly-spacing: off object-property-newline: off one-var-declaration-per-line: off one-var: off operator-assignment: off operator-linebreak: off padded-blocks: off quote-props: off quotes: off require-jsdoc: off semi-spacing: off semi: off sort-keys: off sort-vars: off space-before-blocks: off space-before-function-paren: off space-in-parens: off space-infix-ops: off space-unary-ops: off spaced-comment: off template-tag-spacing: off unicode-bom: off wrap-regex: off # ECMAScript 6 arrow-body-style: off arrow-parens: off arrow-spacing: off constructor-super: off generator-star-spacing: off no-class-assign: off no-confusing-arrow: off no-const-assign: off no-dupe-class-members: off no-duplicate-imports: off no-new-symbol: off no-restricted-imports: off no-this-before-super: off no-useless-computed-key: off no-useless-constructor: off no-useless-rename: off no-var: off object-shorthand: off prefer-arrow-callback: off prefer-const: off prefer-destructuring: off prefer-numeric-literals: off prefer-rest-params: off prefer-reflect: off prefer-spread: off prefer-template: off require-yield: off rest-spread-spacing: off sort-imports: off symbol-description: off template-curly-spacing: off yield-star-spacing: off ================================================ FILE: .flowconfig ================================================ [ignore] .*/node_modules/.* [include] .*/src/.* [options] # Add emojis to status messages because emojis are cool 😎 emoji=true # Enabled strict argument types experimental.strict_type_args=true # Only run flow on the following file extensions module.file_ext=.js module.name_mapper='^push$' -> '/src/push/' module.name_mapper='^types$' -> '/src/types.js' module.name_mapper='^agents$' -> '/src/agents' [version] ^0.57.3 ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io ### OSX ### .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Node ### # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release dist # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules .idea # User-specific stuff: .idea/workspace.xml .idea/tasks.xml # Sensitive or high-churn files: .idea/dataSources/ .idea/dataSources.ids .idea/dataSources.xml .idea/dataSources.local.xml .idea/sqlDataSources.xml .idea/dynamic.xml .idea/uiDesigner.xml # Gradle: .idea/gradle.xml .idea/libraries # Mongo Explorer plugin: .idea/mongoSettings.xml ## File-based project format: *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties ================================================ FILE: .npmignore ================================================ .* ================================================ FILE: .nvmrc ================================================ v8.12 ================================================ FILE: .prettierrc ================================================ singleQuote: true semi: true useTabs: false tabWidth: 4 printWidth: 80 ================================================ FILE: .travis.yml ================================================ env: global: - CC_TEST_REPORTER_ID=71e04b5c32896d7201c79e2db2af1a89fc375f668392802961019875e71b179f - GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct; else git log -1 --skip 1 --pretty=format:%ct; fi) - secure: AwJJp7im+vXzxa/UsYu/EJNppu+ss9l0GW8ftaXLNnMRkLJyo6m24DCJuhfH/ynxrSq5b2Zc7gEqDfxVWUgpNR+rrHZoHe+R/QnL60sqQXE2KvblDvG4o4e9F7vFp1xsohJ3/TTm6footWEiVlP25oVkQgi/oWhFsygJD6VGerDa2CodtU2r4p6UV5KuI8mUZuQg+rndkosMZa1BkZcz6v8e1AmJkGiIl/Agw0ye6C9iav4KoU+EXRyoxEq+dTAuhnVKA3CntOngoPDrUTQy5303x6Gzaz7uByWIyIR+uEud65RvCBduxCgREazkXdCqd/vCS65gYYktxl3fNG2so5VpKAbF/pTOylWexB10xhB+k+alIxhl3QytUx1pqDR4WI6c8d6ot+sZd7AjwvlyWdwDPuLoDB2eA18K3HbFMgjONmJFFI3gyKfyg1z5FfZm6dogfuQGZeSyxuedDAo+FygKGbgvBa+JHerR1WjU+TnFcVpwgaX/sma8Q4ff9WYLw2YOIiSi0H5V5tDi8lrtOqZmIYH4Vv2Cbl1gaAsVOOo+IBfTWor4oJzAb/jRKuNbqvhUJIqW6RYS7f8c/LMrcdn+LWHGj3zcFQ+LNFxID4OCghf7A3FvmE8A5XD07w04ofnWzrgqzgWy5+38Qj23l5IpFNgD2XGNohk7JpeGm3E= language: node_js node_js: - lts/* before_install: yarn global add greenkeeper-lockfile@1 before_script: - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build - greenkeeper-lockfile-update script: - npm run test after_script: - if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi - greenkeeper-lockfile-upload - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guidelines So you want to contribute to Push, huh? Well lucky for you, it's really easy to do so, because you're just dealing with like, a few hundred lines of JavaScript. It's not hard. Alright. Now calm down and take a few deep breaths. Here we go. All you have to remember is two commands... think you can do that? To BUILD Push, just run: ```bash $ npm run build ``` To TEST Push on BrowserStack, run: ```bash $ npm run test ``` See? Not hard at all. Unfortunately the Notifications API doesn't always play nicely with local sites, so don't get discouraged if you try running Push in a local HTML file and it doesn't work. To TEST Push on a specific, locally-installed browser, you can run one of the following: ```bash $ npm run test:opera $ npm run test:firefox $ npm run test:chrome $ npm run test:safari ``` ### Testing & Travis Push uses the [Karma](https://karma-runner.github.io/1.0/index.html) JavaScript test runner, so read up on that if you want to make changes to any of the tests that are run. These tests are run post-push by [Travis CI](https://travis-ci.org), so look into that if you want to make any Travis configuration changes. Although, at this point I'd say Travis is all set. The tests might want to be expanded though. ### REAL IMPORTANT STUFF **THERE IS ONLY ONE RULE TO PUSH CLUB** (and no, it's not that you can't talk about it). **WHENEVER** you make changes to `Push.js`, **RECOMPILE** and commit `push.min.js` as well. Until this build process can be wrapped into a sexy git hook of some sort, this is how changes to the library need to occur. **YOUR PR WILL NOT BE APPROVED UNLESS THIS HAPPENS**. That said, I did let it slide once because I wasn't thinking, but that's why I wrote this file to make sure it will never happen again. Outside of that, contributing should not be at all scary and should be a fun and positive process. Now go out and write some killer JS! Wait... is there even such a thing? ================================================ FILE: LICENSE.md ================================================ # The MIT License (MIT) Copyright (c) 2016 Tyler Nickerson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
[](http://pushjs.org)

[![Build Status](https://img.shields.io/travis/Nickersoft/push.js.svg)](https://travis-ci.org/Nickersoft/push.js) [![Coverage Status](https://img.shields.io/coveralls/Nickersoft/push.js.svg)](https://coveralls.io/github/Nickersoft/push.js?branch=master) [![Known Vulnerabilities](https://snyk.io/test/github/nickersoft/push.js/badge.svg)](https://snyk.io/test/github/nickersoft/push.js) [![Maintainability](https://api.codeclimate.com/v1/badges/52747084d9786c1570df/maintainability)](https://codeclimate.com/github/Nickersoft/push.js/maintainability) [![npm version](https://img.shields.io/npm/v/push.js.svg)](https://npmjs.com/package/push.js) [![npm](https://img.shields.io/npm/dm/push.js.svg)](https://npmjs.com/package/push.js) [![Greenkeeper badge](https://badges.greenkeeper.io/Nickersoft/push.js.svg)](https://greenkeeper.io/) *Now a proud user of* [](https://browserstack.com)
> ## Important Notice > Push is currently looking for co-maintainers of the repo. The guy who originally made this library, [Tyler Nickerson](https://tylernickerson.com), while still visiting this repo from time to time, is busy trying to work on his company [Linguistic](https://github.com/linguistic) right now. As a result, he may not have time to answer everyone or fix bugs as quickly as they would like him too. If you find it pretty easy to find your way around this code and think you could help some people out, shoot me a message at [nickersoft@gmail.com](mailto:nickersoft@gmail.com) and let's talk. ### What is Push? ### Push is the fastest way to get up and running with Javascript desktop notifications. A fairly new addition to the official specification, the Notification API allows modern browsers such as Chrome, Safari, Firefox, and IE 9+ to push notifications to a user's desktop. Push acts as a cross-browser solution to this API, falling back to use older implementations if the user's browser does not support the new API. You can quickly install Push via [npm](http://npmjs.com): ``` npm install push.js --save ``` Or, if you want something a little more lightweight, you can give [Bower](http://bower.io) a try: ``` bower install push.js --save ``` ### Full Documentation ### Full documentation for Push can be found at the project's new homepage [https://pushjs.org](https://pushjs.org). See you there! ### Development ### If you feel like this library is your jam and you want to contribute (or you think I'm an idiot who missed something), check out Push's neat [contributing guidelines](CONTRIBUTING.md) on how you can make your mark. ### Credits ### Push is based off the following work: 1. [HTML5-Desktop-Notifications](https://github.com/ttsvetko/HTML5-Desktop-Notifications) by [Tsvetan Tsvetkov](https://github.com/ttsvetko) 2. [notify.js](https://github.com/alexgibson/notify.js) by [Alex Gibson](https://github.com/alexgibson) ================================================ FILE: bin/push.js ================================================ /** * @license * * Push v1.0.9 * ========= * A compact, cross-browser solution for the JavaScript Notifications API * * Credits * ------- * Tsvetan Tsvetkov (ttsvetko) * Alex Gibson (alexgibson) * * License * ------- * * The MIT License (MIT) * * Copyright (c) 2015-2017 Tyler Nickerson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ !function(i,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(i=i||self).Push=t()}(this,function(){"use strict";var i={errors:{incompatible:"".concat("PushError:"," Push.js is incompatible with browser."),invalid_plugin:"".concat("PushError:"," plugin class missing from plugin manifest (invalid plugin). Please check the documentation."),invalid_title:"".concat("PushError:"," title of notification must be a string"),permission_denied:"".concat("PushError:"," permission request declined"),sw_notification_error:"".concat("PushError:"," could not show a ServiceWorker notification due to the following reason: "),sw_registration_error:"".concat("PushError:"," could not register the ServiceWorker due to the following reason: "),unknown_interface:"".concat("PushError:"," unable to create notification: unknown interface")}};function t(i){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i})(i)}function n(i,t){if(!(i instanceof t))throw new TypeError("Cannot call a class as a function")}function e(i,t){for(var n=0;n0?this._requestWithCallback.apply(this,arguments):this._requestAsPromise()}},{key:"_requestWithCallback",value:function(i,t){var n,e=this,o=this.get(),r=!1,s=function(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:e._win.Notification.permission;r||(r=!0,void 0===n&&e._win.webkitNotifications&&(n=e._win.webkitNotifications.checkPermission()),n===e.GRANTED||0===n?i&&i():t&&t())};o!==this.DEFAULT?s(o):this._win.webkitNotifications&&this._win.webkitNotifications.checkPermission?this._win.webkitNotifications.requestPermission(s):this._win.Notification&&this._win.Notification.requestPermission?(n=this._win.Notification.requestPermission(s))&&n.then&&n.then(s).catch(function(){t&&t()}):i&&i()}},{key:"_requestAsPromise",value:function(){var i=this,t=this.get(),n=t!==this.DEFAULT,e=this._win.Notification&&this._win.Notification.requestPermission,o=this._win.webkitNotifications&&this._win.webkitNotifications.checkPermission;return new Promise(function(r,s){var c,a=!1,u=function(t){a||(a=!0,!function(t){return t===i.GRANTED||0===t}(t)?s():r())};n?u(t):o?i._win.webkitNotifications.requestPermission(function(i){u(i)}):e?(c=i._win.Notification.requestPermission(u))&&c.then&&c.then(u).catch(s):r()})}},{key:"has",value:function(){return this.get()===this.GRANTED}},{key:"get",value:function(){return this._win.Notification&&this._win.Notification.permission?this._win.Notification.permission:this._win.webkitNotifications&&this._win.webkitNotifications.checkPermission?this._permissions[this._win.webkitNotifications.checkPermission()]:navigator.mozNotification?this.GRANTED:this._win.external&&this._win.external.msIsSiteMode?this._win.external.msIsSiteMode()?this.GRANTED:this.DEFAULT:this.GRANTED}}]),i}(),f=function(){function i(){n(this,i)}return o(i,null,[{key:"isUndefined",value:function(i){return void 0===i}},{key:"isNull",value:function(i){return null===obj}},{key:"isString",value:function(i){return"string"==typeof i}},{key:"isFunction",value:function(i){return i&&"[object Function]"==={}.toString.call(i)}},{key:"isObject",value:function(i){return"object"===t(i)}},{key:"objectMerge",value:function(i,t){for(var n in t)i.hasOwnProperty(n)&&this.isObject(i[n])&&this.isObject(t[n])?this.objectMerge(i[n],t[n]):i[n]=t[n]}}]),i}(),l=function i(t){n(this,i),this._win=t},h=function(i){function t(){return n(this,t),a(this,s(t).apply(this,arguments))}return r(t,l),o(t,[{key:"isSupported",value:function(){return void 0!==this._win.Notification}},{key:"create",value:function(i,t){return new this._win.Notification(i,{icon:f.isString(t.icon)||f.isUndefined(t.icon)||f.isNull(t.icon)?t.icon:t.icon.x32,body:t.body,tag:t.tag,requireInteraction:t.requireInteraction})}},{key:"close",value:function(i){i.close()}}]),t}(),_=function(t){function e(){return n(this,e),a(this,s(e).apply(this,arguments))}return r(e,l),o(e,[{key:"isSupported",value:function(){return void 0!==this._win.navigator&&void 0!==this._win.navigator.serviceWorker}},{key:"getFunctionBody",value:function(i){var t=i.toString().match(/function[^{]+{([\s\S]*)}$/);return null!=t&&t.length>1?t[1]:null}},{key:"create",value:function(t,n,e,o,r){var s=this;this._win.navigator.serviceWorker.register(o),this._win.navigator.serviceWorker.ready.then(function(o){var c={id:t,link:e.link,origin:document.location.href,onClick:f.isFunction(e.onClick)?s.getFunctionBody(e.onClick):"",onClose:f.isFunction(e.onClose)?s.getFunctionBody(e.onClose):""};void 0!==e.data&&null!==e.data&&(c=Object.assign(c,e.data)),o.showNotification(n,{icon:e.icon,body:e.body,vibrate:e.vibrate,tag:e.tag,data:c,requireInteraction:e.requireInteraction,silent:e.silent}).then(function(){o.getNotifications().then(function(i){o.active.postMessage(""),r(i)})}).catch(function(t){throw new Error(i.errors.sw_notification_error+t.message)})}).catch(function(t){throw new Error(i.errors.sw_registration_error+t.message)})}},{key:"close",value:function(){}}]),e}(),v=function(i){function t(){return n(this,t),a(this,s(t).apply(this,arguments))}return r(t,l),o(t,[{key:"isSupported",value:function(){return void 0!==this._win.navigator.mozNotification}},{key:"create",value:function(i,t){var n=this._win.navigator.mozNotification.createNotification(i,t.body,t.icon);return n.show(),n}}]),t}(),d=function(i){function t(){return n(this,t),a(this,s(t).apply(this,arguments))}return r(t,l),o(t,[{key:"isSupported",value:function(){return void 0!==this._win.external&&void 0!==this._win.external.msIsSiteMode}},{key:"create",value:function(i,t){return this._win.external.msSiteModeClearIconOverlay(),this._win.external.msSiteModeSetIconOverlay(f.isString(t.icon)||f.isUndefined(t.icon)?t.icon:t.icon.x16,i),this._win.external.msSiteModeActivate(),null}},{key:"close",value:function(){this._win.external.msSiteModeClearIconOverlay()}}]),t}(),w=function(i){function t(){return n(this,t),a(this,s(t).apply(this,arguments))}return r(t,l),o(t,[{key:"isSupported",value:function(){return void 0!==this._win.webkitNotifications}},{key:"create",value:function(i,t){var n=this._win.webkitNotifications.createNotification(t.icon,i,t.body);return n.show(),n}},{key:"close",value:function(i){i.cancel()}}]),t}();return new(function(){function t(i){n(this,t),this._currentId=0,this._notifications={},this._win=i,this.Permission=new u(i),this._agents={desktop:new h(i),chrome:new _(i),firefox:new v(i),ms:new d(i),webkit:new w(i)},this._configuration={serviceWorker:"/serviceWorker.min.js",fallback:function(i){}}}return o(t,[{key:"_closeNotification",value:function(t){var n=!0,e=this._notifications[t];if(void 0!==e){if(n=this._removeNotification(t),this._agents.desktop.isSupported())this._agents.desktop.close(e);else if(this._agents.webkit.isSupported())this._agents.webkit.close(e);else{if(!this._agents.ms.isSupported())throw n=!1,new Error(i.errors.unknown_interface);this._agents.ms.close()}return n}return!1}},{key:"_addNotification",value:function(i){var t=this._currentId;return this._notifications[t]=i,this._currentId++,t}},{key:"_removeNotification",value:function(i){var t=!1;return this._notifications.hasOwnProperty(i)&&(delete this._notifications[i],t=!0),t}},{key:"_prepareNotification",value:function(i,t){var n,e=this;return n={get:function(){return e._notifications[i]},close:function(){e._closeNotification(i)}},t.timeout&&setTimeout(function(){n.close()},t.timeout),n}},{key:"_serviceWorkerCallback",value:function(i,t,n){var e=this,o=this._addNotification(i[i.length-1]);navigator&&navigator.serviceWorker&&(navigator.serviceWorker.addEventListener("message",function(i){var t=JSON.parse(i.data);"close"===t.action&&Number.isInteger(t.id)&&e._removeNotification(t.id)}),n(this._prepareNotification(o,t))),n(null)}},{key:"_createCallback",value:function(i,t,n){var e,o=this,r=null;if(t=t||{},e=function(i){o._removeNotification(i),f.isFunction(t.onClose)&&t.onClose.call(o,r)},this._agents.desktop.isSupported())try{r=this._agents.desktop.create(i,t)}catch(e){var s=this._currentId,c=this.config().serviceWorker;this._agents.chrome.isSupported()&&this._agents.chrome.create(s,i,t,c,function(i){return o._serviceWorkerCallback(i,t,n)})}else this._agents.webkit.isSupported()?r=this._agents.webkit.create(i,t):this._agents.firefox.isSupported()?this._agents.firefox.create(i,t):this._agents.ms.isSupported()?r=this._agents.ms.create(i,t):(t.title=i,this.config().fallback(t));if(null!==r){var a=this._addNotification(r),u=this._prepareNotification(a,t);f.isFunction(r.addEventListener)&&(f.isFunction(t.onShow)&&r.addEventListener("show",t.onShow),f.isFunction(t.onError)&&r.addEventListener("error",t.onError),f.isFunction(t.onClick)&&r.addEventListener("click",t.onClick),r.addEventListener("close",function(){e(a)}),r.addEventListener("cancel",function(){e(a)})),n(u)}n(null)}},{key:"create",value:function(t,n){var e,o=this;if(!f.isString(t))throw new Error(i.errors.invalid_title);return e=this.Permission.has()?function(i,e){try{o._createCallback(t,n,i)}catch(i){e(i)}}:function(e,r){o.Permission.request().then(function(){o._createCallback(t,n,e)}).catch(function(){r(i.errors.permission_denied)})},new Promise(e)}},{key:"count",value:function(){var i,t=0;for(i in this._notifications)this._notifications.hasOwnProperty(i)&&t++;return t}},{key:"close",value:function(i){var t;for(t in this._notifications)if(this._notifications.hasOwnProperty(t)&&this._notifications[t].tag===i)return this._closeNotification(t)}},{key:"clear",value:function(){var i,t=!0;for(i in this._notifications)this._notifications.hasOwnProperty(i)&&(t=t&&this._closeNotification(i));return t}},{key:"supported",value:function(){var i=!1;for(var t in this._agents)this._agents.hasOwnProperty(t)&&(i=i||this._agents[t].isSupported());return i}},{key:"config",value:function(i){return(void 0!==i||null!==i&&f.isObject(i))&&f.objectMerge(this._configuration,i),this._configuration}},{key:"extend",value:function(t){var n,e={}.hasOwnProperty;if(!e.call(t,"plugin"))throw new Error(i.errors.invalid_plugin);for(var o in e.call(t,"config")&&f.isObject(t.config)&&null!==t.config&&this.config(t.config),n=new(0,t.plugin)(this.config()))e.call(n,o)&&f.isFunction(n[o])&&(this[o]=n[o])}}]),t}())("undefined"!=typeof window?window:global)}); //# sourceMappingURL=push.js.map ================================================ FILE: bower.json ================================================ { "name": "push.js", "description": "A compact, cross-browser solution for the Javascript Notifications API", "main": "bin/push.js", "authors": [ "Tyler Nickerson" ], "license": "MIT", "homepage": "https://pushjs.org", "ignore": [ "**/.*", "coverage", "node_modules", "bower_components", "tests", "src", "build", "*.lock", "*.json" ] } ================================================ FILE: index.d.ts ================================================ declare module 'push.js' { const defaultPush: Push; export default defaultPush; class Push { Permission: PushPermission; create(title: string, params?: PushNotificationParams): Promise close(tag: string): void; clear(): void; config(params: PushParams): void; } export interface PushNotificationParams { body?: string; icon?: string; link?: string; timeout?: number; tag?: string; requireInteraction?: boolean; vibrate?: boolean; silent?: boolean; onClick?: Function; onError?: Function; } export interface PushParams { serviceWorker?: string; fallback?: Function; } export interface PushPermission { DEFAULT: string; GRANTED: string; DENIED: string; request(onGranted?: Function, onDenied?: Function): void; has(): boolean; get(): string; } export interface PushNotification { close(): void; } } ================================================ FILE: package.json ================================================ { "name": "push.js", "version": "1.0.12", "description": "A compact, cross-browser solution for the Javascript Notifications API", "main": "bin/push.min.js", "scripts": { "clean": "rimraf bin/", "build": "rollup -c && uglifyjs --source-map -o bin/serviceWorker.min.js src/serviceWorker.js", "test": "npm run build && karma start tests/karma.conf.js", "test:chrome": "PUSHJS_TEST_BROWSER=Chrome npm run test", "test:opera": "PUSHJS_TEST_BROWSER=Opera npm run test", "test:firefox": "PUSHJS_TEST_BROWSER=Firefox npm run test", "test:safari": "PUSHJS_TEST_BROWSER=Safari npm run test", "prepublish": "npm run build", "precommit": "lint-staged && npm run build && git add ./bin" }, "files": [ "bin", "*.md", "*.png", "*.d.ts" ], "repository": { "type": "git", "url": "https://github.com/Nickersoft/push.js" }, "author": "Tyler Nickerson", "license": "MIT", "bugs": { "url": "https://github.com/Nickersoft/push.js/issues" }, "homepage": "https://github.com/Nickersoft/push.js", "devDependencies": { "@babel/core": "^7.5.5", "@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-decorators": "^7.4.4", "@babel/plugin-proposal-export-namespace-from": "^7.5.2", "@babel/plugin-proposal-function-sent": "^7.5.0", "@babel/plugin-proposal-json-strings": "^7.2.0", "@babel/plugin-proposal-numeric-separator": "^7.2.0", "@babel/plugin-proposal-throw-expressions": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/plugin-transform-flow-strip-types": "^7.4.4", "@babel/plugin-transform-strict-mode": "^7.2.0", "@babel/polyfill": "^7.4.4", "@babel/preset-env": "^7.5.5", "@babel/preset-flow": "^7.0.0", "browserify": "^16.3.0", "coveralls": "^3.0.5", "flow-bin": "^0.103.0", "husky": "^3.0.1", "jasmine-core": "^3.4.0", "js-yaml": "^3.13.1", "karma": "^4.2.0", "karma-browserstack-launcher": "~1.4.0", "karma-chrome-launcher": "^3.0.0", "karma-coverage": "^1.1.2", "karma-firefox-launcher": "^1.1.0", "karma-jasmine": "^2.0.1", "karma-mocha-reporter": "^2.2.5", "karma-opera-launcher": "^1.0.0", "karma-safari-launcher": "^1.0.0", "karma-sourcemap-loader": "^0.3.7", "lint-staged": "^9.2.0", "natives": "^1.1.6", "platform": "^1.3.5", "prettier": "^1.18.2", "rimraf": "^2.6.3", "rollup": "^1.17.0", "rollup-plugin-alias": "^1.5.2", "rollup-plugin-babel": "^4.3.3", "rollup-plugin-commonjs": "^10.0.1", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-terser": "^5.1.1" }, "lint-staged": { "*.{js,json,css}": [ "prettier --write", "git add" ] }, "dependencies": {}, "resolutions": { "natives": "1.1.6" } } ================================================ FILE: rollup.config.js ================================================ import path from "path"; import resolve from "rollup-plugin-node-resolve"; import babel from "rollup-plugin-babel"; import commonjs from "rollup-plugin-commonjs"; import alias from "rollup-plugin-alias"; import { terser } from "rollup-plugin-terser"; const license = `/** * @license * * Push v1.0.9 * ========= * A compact, cross-browser solution for the JavaScript Notifications API * * Credits * ------- * Tsvetan Tsvetkov (ttsvetko) * Alex Gibson (alexgibson) * * License * ------- * * The MIT License (MIT) * * Copyright (c) 2015-2017 Tyler Nickerson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */`; const common = { input: "src/index.js", output: { banner: license, file: "bin/push.min.js", format: "umd", name: "Push", sourcemap: true }, plugins: [ babel({ exclude: "node_modules/**" }), alias({ types: path.resolve(__dirname, "src/types"), push: path.resolve(__dirname, "src/push/index"), agents: path.resolve(__dirname, "src/agents/index") }), commonjs(), resolve(), terser({ output: { beautify: false, preamble: license } }) ] }; export default [ { ...common, output: { ...common.output, file: "bin/push.js" } }, { ...common, output: { ...common.output, file: "bin/push.min.js" } } ]; ================================================ FILE: src/agents/AbstractAgent.js ================================================ // @flow import type { Global } from 'types'; export default class AbstractAgent { _win: Global; constructor(win: Global) { this._win = win; } } ================================================ FILE: src/agents/DesktopAgent.js ================================================ // @flow import { AbstractAgent } from 'agents'; import { Util } from 'push'; import type { PushOptions, GenericNotification, Global } from 'types'; /** * Notification agent for modern desktop browsers: * Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */ export default class DesktopAgent extends AbstractAgent { _win: Global; /** * Returns a boolean denoting support * @returns {Boolean} boolean denoting whether webkit notifications are supported */ isSupported() { return this._win.Notification !== undefined; } /** * Creates a new notification * @param title - notification title * @param options - notification options array * @returns {Notification} */ create(title: string, options: PushOptions) { return new this._win.Notification(title, { icon: Util.isString(options.icon) || Util.isUndefined(options.icon) || Util.isNull(options.icon) ? options.icon : options.icon.x32, body: options.body, tag: options.tag, requireInteraction: options.requireInteraction }); } /** * Close a given notification * @param notification - notification to close */ close(notification: GenericNotification) { notification.close(); } } ================================================ FILE: src/agents/MSAgent.js ================================================ // @flow import { AbstractAgent } from 'agents'; import { Util } from 'push'; import type { PushOptions, Global } from 'types'; /** * Notification agent for IE9 */ export default class MSAgent extends AbstractAgent { _win: Global; /** * Returns a boolean denoting support * @returns {Boolean} boolean denoting whether webkit notifications are supported */ isSupported() { return ( this._win.external !== undefined && this._win.external.msIsSiteMode !== undefined ); } /** * Creates a new notification * @param title - notification title * @param options - notification options array * @returns {Notification} */ create(title: string, options: PushOptions) { /* Clear any previous notifications */ this._win.external.msSiteModeClearIconOverlay(); this._win.external.msSiteModeSetIconOverlay( Util.isString(options.icon) || Util.isUndefined(options.icon) ? options.icon : options.icon.x16, title ); this._win.external.msSiteModeActivate(); return null; } /** * Close a given notification * @param notification - notification to close */ close() { this._win.external.msSiteModeClearIconOverlay(); } } ================================================ FILE: src/agents/MobileChromeAgent.js ================================================ // @flow import { Util, Messages } from 'push'; import { AbstractAgent } from 'agents'; import type { Global, GenericNotification, PushOptions } from 'types'; /** * Notification agent for modern desktop browsers: * Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */ export default class MobileChromeAgent extends AbstractAgent { _win: Global; /** * Returns a boolean denoting support * @returns {Boolean} boolean denoting whether webkit notifications are supported */ isSupported() { return ( this._win.navigator !== undefined && this._win.navigator.serviceWorker !== undefined ); } /** * Returns the function body as a string * @param func */ getFunctionBody(func: () => void) { const str = func.toString().match(/function[^{]+{([\s\S]*)}$/); return typeof str !== 'undefined' && str !== null && str.length > 1 ? str[1] : null; } /** * Creates a new notification * @param id ID of notification * @param title Title of notification * @param options Options object * @param serviceWorker ServiceWorker path * @param callback Callback function */ create( id: number, title: string, options: PushOptions, serviceWorker: string, callback: (GenericNotification[]) => void ) { /* Register ServiceWorker */ this._win.navigator.serviceWorker.register(serviceWorker); this._win.navigator.serviceWorker.ready .then(registration => { /* Local data the service worker will use */ let localData = { id: id, link: options.link, origin: document.location.href, onClick: Util.isFunction(options.onClick) ? this.getFunctionBody(options.onClick) : '', onClose: Util.isFunction(options.onClose) ? this.getFunctionBody(options.onClose) : '' }; /* Merge the local data with user-provided data */ if (options.data !== undefined && options.data !== null) localData = Object.assign(localData, options.data); /* Show the notification */ registration .showNotification(title, { icon: options.icon, body: options.body, vibrate: options.vibrate, tag: options.tag, data: localData, requireInteraction: options.requireInteraction, silent: options.silent }) .then(() => { registration.getNotifications().then(notifications => { /* Send an empty message so the ServiceWorker knows who the client is */ registration.active.postMessage(''); /* Trigger callback */ callback(notifications); }); }) .catch(function(error) { throw new Error( Messages.errors.sw_notification_error + error.message ); }); }) .catch(function(error) { throw new Error( Messages.errors.sw_registration_error + error.message ); }); } /** * Close all notification */ close() { // Can't do this with service workers } } ================================================ FILE: src/agents/MobileFirefoxAgent.js ================================================ // @flow import { AbstractAgent } from 'agents'; import type { Global, PushOptions } from 'types'; /** * Notification agent for modern desktop browsers: * Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */ export default class MobileFirefoxAgent extends AbstractAgent { _win: Global; /** * Returns a boolean denoting support * @returns {Boolean} boolean denoting whether webkit notifications are supported */ isSupported() { return this._win.navigator.mozNotification !== undefined; } /** * Creates a new notification * @param title - notification title * @param options - notification options array * @returns {Notification} */ create(title: string, options: PushOptions) { let notification = this._win.navigator.mozNotification.createNotification( title, options.body, options.icon ); notification.show(); return notification; } } ================================================ FILE: src/agents/WebKitAgent.js ================================================ // @flow import { AbstractAgent } from 'agents'; import type { Global, GenericNotification, PushOptions } from 'types'; /** * Notification agent for old Chrome versions (and some) Firefox */ export default class WebKitAgent extends AbstractAgent { _win: Global; /** * Returns a boolean denoting support * @returns {Boolean} boolean denoting whether webkit notifications are supported */ isSupported() { return this._win.webkitNotifications !== undefined; } /** * Creates a new notification * @param title - notification title * @param options - notification options array * @returns {Notification} */ create(title: string, options: PushOptions) { let notification = this._win.webkitNotifications.createNotification( options.icon, title, options.body ); notification.show(); return notification; } /** * Close a given notification * @param notification - notification to close */ close(notification: GenericNotification) { notification.cancel(); } } ================================================ FILE: src/agents/index.js ================================================ import AbstractAgent from './AbstractAgent'; import DesktopAgent from './DesktopAgent'; import MobileChromeAgent from './MobileChromeAgent'; import MobileFirefoxAgent from './MobileFirefoxAgent'; import MSAgent from './MSAgent'; import WebKitAgent from './WebKitAgent'; export { AbstractAgent, DesktopAgent, MobileChromeAgent, MobileFirefoxAgent, MSAgent, WebKitAgent }; ================================================ FILE: src/index.js ================================================ // @flow import { Push } from 'push'; export default new Push(typeof window !== 'undefined' ? window : global); ================================================ FILE: src/push/Messages.js ================================================ // @flow const errorPrefix = 'PushError:'; export default { errors: { incompatible: `${errorPrefix} Push.js is incompatible with browser.`, invalid_plugin: `${errorPrefix} plugin class missing from plugin manifest (invalid plugin). Please check the documentation.`, invalid_title: `${errorPrefix} title of notification must be a string`, permission_denied: `${errorPrefix} permission request declined`, sw_notification_error: `${errorPrefix} could not show a ServiceWorker notification due to the following reason: `, sw_registration_error: `${errorPrefix} could not register the ServiceWorker due to the following reason: `, unknown_interface: `${errorPrefix} unable to create notification: unknown interface` } }; ================================================ FILE: src/push/Permission.js ================================================ // @flow import type { Global } from 'types'; export default class Permission { // Private members _permissions: string[]; _win: Global; // Public members GRANTED: string; DEFAULT: string; DENIED: string; constructor(win: Global) { this._win = win; this.GRANTED = 'granted'; this.DEFAULT = 'default'; this.DENIED = 'denied'; this._permissions = [this.GRANTED, this.DEFAULT, this.DENIED]; } /** * Requests permission for desktop notifications * @param {Function} onGranted - Function to execute once permission is granted * @param {Function} onDenied - Function to execute once permission is denied * @return {void, Promise} */ request(onGranted: () => void, onDenied: () => void) { return arguments.length > 0 ? this._requestWithCallback(...arguments) : this._requestAsPromise(); } /** * Old permissions implementation deprecated in favor of a promise based one * @deprecated Since V1.0.4 * @param {Function} onGranted - Function to execute once permission is granted * @param {Function} onDenied - Function to execute once permission is denied * @return {void} */ _requestWithCallback(onGranted: () => void, onDenied: () => void) { const existing = this.get(); var resolved = false; var resolve = (result = this._win.Notification.permission) => { if (resolved) return; resolved = true; if (typeof result === 'undefined' && this._win.webkitNotifications) result = this._win.webkitNotifications.checkPermission(); if (result === this.GRANTED || result === 0) { if (onGranted) onGranted(); } else if (onDenied) onDenied(); }; var request; /* Permissions already set */ if (existing !== this.DEFAULT) { resolve(existing); } else if ( this._win.webkitNotifications && this._win.webkitNotifications.checkPermission ) { /* Safari 6+, Legacy webkit browsers */ this._win.webkitNotifications.requestPermission(resolve); } else if ( this._win.Notification && this._win.Notification.requestPermission ) { /* Safari 12+ */ /* This resolve argument will only be used in Safari */ /* CHrome, instead, returns a Promise */ request = this._win.Notification.requestPermission(resolve); if (request && request.then) { /* Chrome 23+ */ request.then(resolve).catch(function() { if (onDenied) onDenied(); }); } } else if (onGranted) { /* Let the user continue by default */ onGranted(); } } /** * Requests permission for desktop notifications in a promise based way * @return {Promise} */ _requestAsPromise(): Promise { const existing = this.get(); let isGranted = result => result === this.GRANTED || result === 0; /* Permissions already set */ var hasPermissions = existing !== this.DEFAULT; /* Safari 6+, Chrome 23+ */ var isModernAPI = this._win.Notification && this._win.Notification.requestPermission; /* Legacy webkit browsers */ var isWebkitAPI = this._win.webkitNotifications && this._win.webkitNotifications.checkPermission; return new Promise((resolvePromise, rejectPromise) => { var resolved = false; var resolver = result => { if (resolved) return; resolved = true; isGranted(result) ? resolvePromise() : rejectPromise(); }; var request; if (hasPermissions) { resolver(existing); } else if (isWebkitAPI) { this._win.webkitNotifications.requestPermission(result => { resolver(result); }); } else if (isModernAPI) { /* Safari 12+ */ /* This resolver argument will only be used in Safari */ /* CHrome, instead, returns a Promise */ request = this._win.Notification.requestPermission(resolver); if (request && request.then) { /* Chrome 23+ */ request.then(resolver).catch(rejectPromise); } } else resolvePromise(); }); } /** * Returns whether Push has been granted permission to run * @return {Boolean} */ has() { return this.get() === this.GRANTED; } /** * Gets the permission level * @return {Permission} The permission level */ get() { let permission; /* Safari 6+, Chrome 23+ */ if (this._win.Notification && this._win.Notification.permission) permission = this._win.Notification.permission; else if ( this._win.webkitNotifications && this._win.webkitNotifications.checkPermission ) /* Legacy webkit browsers */ permission = this._permissions[ this._win.webkitNotifications.checkPermission() ]; else if (navigator.mozNotification) /* Firefox Mobile */ permission = this.GRANTED; else if (this._win.external && this._win.external.msIsSiteMode) /* IE9+ */ permission = this._win.external.msIsSiteMode() ? this.GRANTED : this.DEFAULT; else permission = this.GRANTED; return permission; } } ================================================ FILE: src/push/Push.js ================================================ // @flow import { Messages, Permission, Util } from 'push'; import type { PluginManifest, GenericNotification, PushOptions } from 'types'; /* Import notification agents */ import { DesktopAgent, MobileChromeAgent, MobileFirefoxAgent, MSAgent, WebKitAgent } from 'agents'; export default class Push { // Private members _agents: { desktop: DesktopAgent, chrome: MobileChromeAgent, firefox: MobileFirefoxAgent, ms: MSAgent, webkit: WebKitAgent }; _configuration: { serviceWorker: string, fallback: ({}) => void }; _currentId: number; _notifications: {}; _win: {}; // Public members Permission: Permission; constructor(win: {}) { /* Private variables */ /* ID to use for new notifications */ this._currentId = 0; /* Map of open notifications */ this._notifications = {}; /* Window object */ this._win = win; /* Public variables */ this.Permission = new Permission(win); /* Agents */ this._agents = { desktop: new DesktopAgent(win), chrome: new MobileChromeAgent(win), firefox: new MobileFirefoxAgent(win), ms: new MSAgent(win), webkit: new WebKitAgent(win) }; this._configuration = { serviceWorker: '/serviceWorker.min.js', fallback: function(payload) {} }; } /** * Closes a notification * @param id ID of notification * @returns {boolean} denotes whether the operation was successful * @private */ _closeNotification(id: number | string) { let success = true; const notification = this._notifications[id]; if (notification !== undefined) { success = this._removeNotification(id); /* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */ if (this._agents.desktop.isSupported()) this._agents.desktop.close(notification); else if (this._agents.webkit.isSupported()) /* Legacy WebKit browsers */ this._agents.webkit.close(notification); else if (this._agents.ms.isSupported()) /* IE9 */ this._agents.ms.close(); else { success = false; throw new Error(Messages.errors.unknown_interface); } return success; } return false; } /** * Adds a notification to the global dictionary of notifications * @param {Notification} notification * @return {Integer} Dictionary key of the notification * @private */ _addNotification(notification: GenericNotification) { const id = this._currentId; this._notifications[id] = notification; this._currentId++; return id; } /** * Removes a notification with the given ID * @param {Integer} id - Dictionary key/ID of the notification to remove * @return {Boolean} boolean denoting success * @private */ _removeNotification(id: number | string) { let success = false; if (this._notifications.hasOwnProperty(id)) { /* We're successful if we omit the given ID from the new array */ delete this._notifications[id]; success = true; } return success; } /** * Creates the wrapper for a given notification * * @param {Integer} id - Dictionary key/ID of the notification * @param {Map} options - Options used to create the notification * @returns {Map} wrapper hashmap object * @private */ _prepareNotification(id: number, options: PushOptions) { let wrapper; /* Wrapper used to get/close notification later on */ wrapper = { get: () => { return this._notifications[id]; }, close: () => { this._closeNotification(id); } }; /* Autoclose timeout */ if (options.timeout) { setTimeout(() => { wrapper.close(); }, options.timeout); } return wrapper; } /** * Find the most recent notification from a ServiceWorker and add it to the global array * @param notifications * @private */ _serviceWorkerCallback( notifications: GenericNotification[], options: PushOptions, resolve: ({} | null) => void ) { let id = this._addNotification(notifications[notifications.length - 1]); /* Listen for close requests from the ServiceWorker */ if (navigator && navigator.serviceWorker) { navigator.serviceWorker.addEventListener('message', event => { const data = JSON.parse(event.data); if (data.action === 'close' && Number.isInteger(data.id)) this._removeNotification(data.id); }); resolve(this._prepareNotification(id, options)); } resolve(null); } /** * Callback function for the 'create' method * @return {void} * @private */ _createCallback( title: string, options: PushOptions, resolve: ({} | null) => void ) { let notification = null; let onClose; /* Set empty settings if none are specified */ options = options || {}; /* onClose event handler */ onClose = id => { /* A bit redundant, but covers the cases when close() isn't explicitly called */ this._removeNotification(id); if (Util.isFunction(options.onClose)) { options.onClose.call(this, notification); } }; /* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */ if (this._agents.desktop.isSupported()) { try { /* Create a notification using the API if possible */ notification = this._agents.desktop.create(title, options); } catch (e) { const id = this._currentId; const sw = this.config().serviceWorker; const cb = notifications => this._serviceWorkerCallback( notifications, options, resolve ); /* Create a Chrome ServiceWorker notification if it isn't supported */ if (this._agents.chrome.isSupported()) { this._agents.chrome.create(id, title, options, sw, cb); } } /* Legacy WebKit browsers */ } else if (this._agents.webkit.isSupported()) notification = this._agents.webkit.create(title, options); else if (this._agents.firefox.isSupported()) /* Firefox Mobile */ this._agents.firefox.create(title, options); else if (this._agents.ms.isSupported()) /* IE9 */ notification = this._agents.ms.create(title, options); else { /* Default fallback */ options.title = title; this.config().fallback(options); } if (notification !== null) { const id = this._addNotification(notification); const wrapper = this._prepareNotification(id, options); /* Notification callbacks */ if (Util.isFunction(notification.addEventListener)) { if (Util.isFunction(options.onShow)) notification.addEventListener('show', options.onShow); if (Util.isFunction(options.onError)) notification.addEventListener('error', options.onError); if (Util.isFunction(options.onClick)) notification.addEventListener('click', options.onClick); notification.addEventListener('close', () => { onClose(id); }); notification.addEventListener('cancel', () => { onClose(id); }); } /* Return the wrapper so the user can call close() */ resolve(wrapper); } /* By default, pass an empty wrapper */ resolve(null); } /** * Creates and displays a new notification * @param {Array} options * @return {Promise} */ create(title: string, options: {}): Promise { let promiseCallback; /* Fail if no or an invalid title is provided */ if (!Util.isString(title)) { throw new Error(Messages.errors.invalid_title); } /* Request permission if it isn't granted */ if (!this.Permission.has()) { promiseCallback = (resolve: () => void, reject: string => void) => { this.Permission .request() .then(() => { this._createCallback(title, options, resolve); }) .catch(() => { reject(Messages.errors.permission_denied); }); }; } else { promiseCallback = (resolve: () => void, reject: string => void) => { try { this._createCallback(title, options, resolve); } catch (e) { reject(e); } }; } return new Promise(promiseCallback); } /** * Returns the notification count * @return {Integer} The notification count */ count() { let count = 0; let key; for (key in this._notifications) if (this._notifications.hasOwnProperty(key)) count++; return count; } /** * Closes a notification with the given tag * @param {String} tag - Tag of the notification to close * @return {Boolean} boolean denoting success */ close(tag: string) { let key, notification; for (key in this._notifications) { if (this._notifications.hasOwnProperty(key)) { notification = this._notifications[key]; /* Run only if the tags match */ if (notification.tag === tag) { /* Call the notification's close() method */ return this._closeNotification(key); } } } } /** * Clears all notifications * @return {Boolean} boolean denoting whether the clear was successful in closing all notifications */ clear() { let key, success = true; for (key in this._notifications) if (this._notifications.hasOwnProperty(key)) success = success && this._closeNotification(key); return success; } /** * Denotes whether Push is supported in the current browser * @returns {boolean} */ supported() { let supported = false; for (var agent in this._agents) if (this._agents.hasOwnProperty(agent)) supported = supported || this._agents[agent].isSupported(); return supported; } /** * Modifies settings or returns all settings if no parameter passed * @param settings */ config(settings?: {}) { if ( typeof settings !== 'undefined' || (settings !== null && Util.isObject(settings)) ) Util.objectMerge(this._configuration, settings); return this._configuration; } /** * Copies the functions from a plugin to the main library * @param plugin */ extend(manifest: PluginManifest) { var plugin, Plugin, hasProp = {}.hasOwnProperty; if (!hasProp.call(manifest, 'plugin')) { throw new Error(Messages.errors.invalid_plugin); } else { if ( hasProp.call(manifest, 'config') && Util.isObject(manifest.config) && manifest.config !== null ) { this.config(manifest.config); } Plugin = manifest.plugin; plugin = new Plugin(this.config()); for (var member in plugin) { if ( hasProp.call(plugin, member) && Util.isFunction(plugin[member]) ) // $FlowFixMe this[member] = plugin[member]; } } } } ================================================ FILE: src/push/Util.js ================================================ // @flow export default class Util { static isUndefined(obj) { return obj === undefined; } static isNull(obs) { return obj === null; } static isString(obj) { return typeof obj === 'string'; } static isFunction(obj) { return obj && {}.toString.call(obj) === '[object Function]'; } static isObject(obj) { return typeof obj === 'object'; } static objectMerge(target, source) { for (var key in source) { if ( target.hasOwnProperty(key) && this.isObject(target[key]) && this.isObject(source[key]) ) { this.objectMerge(target[key], source[key]); } else { target[key] = source[key]; } } } } ================================================ FILE: src/push/index.js ================================================ import Messages from './Messages'; import Permission from './Permission'; import Util from './Util'; import Push from './Push'; export { Messages, Permission, Util, Push }; ================================================ FILE: src/serviceWorker.js ================================================ 'use strict'; function isFunction(obj) { return obj && {}.toString.call(obj) === '[object Function]'; } function runFunctionString(funcStr) { if (funcStr.trim().length > 0) { var func = new Function(funcStr); if (isFunction(func)) { func(); } } } self.addEventListener('message', function(event) { self.client = event.source; }); self.onnotificationclose = function(event) { runFunctionString(event.notification.data.onClose); /* Tell Push to execute close callback */ self.client.postMessage( JSON.stringify({ id: event.notification.data.id, action: 'close' }) ); }; self.onnotificationclick = function(event) { var link, origin, href; if ( typeof event.notification.data.link !== 'undefined' && event.notification.data.link !== null ) { origin = event.notification.data.origin; link = event.notification.data.link; href = origin.substring(0, origin.indexOf('/', 8)) + '/'; /* Removes prepending slash, as we don't need it */ if (link[0] === '/') { link = link.length > 1 ? link.substring(1, link.length) : ''; } event.notification.close(); /* This looks to see if the current is already open and focuses if it is */ event.waitUntil( clients .matchAll({ type: 'window' }) .then(function(clientList) { var client, full_url; for (var i = 0; i < clientList.length; i++) { client = clientList[i]; full_url = href + link; /* Covers case where full_url might be http://example.com/john and the client URL is http://example.com/john/ */ if ( full_url[full_url.length - 1] !== '/' && client.url[client.url.length - 1] === '/' ) { full_url += '/'; } if (client.url === full_url && 'focus' in client) { return client.focus(); } } if (clients.openWindow) { return clients.openWindow('/' + link); } }) .catch(function(error) { throw new Error( 'A ServiceWorker error occurred: ' + error.message ); }) ); } runFunctionString(event.notification.data.onClick); }; ================================================ FILE: src/types.js ================================================ export type GenericNotification = Notification | webkitNotifications; export type Global = { Notification?: Notification, webkitNotifications?: webkitNotifications }; export type PushOptions = { body?: string, icon?: string, link?: string, timeout?: number, tag?: string, requireInteraction?: boolean, vibrate?: boolean, silent?: boolean, onClick?: Function, onError?: Function }; export type PluginManifest = { plugin: {}, config?: {} }; ================================================ FILE: tests/browsers.conf.js ================================================ var BROWSER_FIREFOX = 'Firefox', BROWSER_CHROME = 'Chrome', BROWSER_EDGE = 'Edge', BROWSER_IE = 'ie', BROWSER_OPERA = 'Opera', BROWSER_SAFARI = 'Safari'; function getWindowsDesktop(browser, version) { return { base: 'BrowserStack', browser: browser, browser_version: version.toString() + '.0', os: 'Windows', os_version: '10' }; } function getOSXDesktop(browser, version, os) { var rounded; if (!os) os = version < 25 ? 'Snow Leopard' : 'Sierra'; rounded = Math.floor(version); if (version == rounded) version = version.toString() + '.0'; return { base: 'BrowserStack', browser: browser, browser_version: version, os: 'OS X', os_version: os }; } function getMobile(browser) { return { base: 'BrowserStack', real_mobile: true, device: 'Google Pixel', browser: 'Mobile ' + browser, os: 'android', os_version: '7.1' }; } module.exports = { bs_firefox_mac: getOSXDesktop(BROWSER_FIREFOX, 54), bs_firefox_mac_old: getOSXDesktop(BROWSER_FIREFOX, 21), bs_chrome_mac: getOSXDesktop(BROWSER_CHROME, 59), bs_edge_win: getWindowsDesktop(BROWSER_EDGE, 15), bs_safari_mac: getOSXDesktop(BROWSER_SAFARI, 10.1, 'Sierra'), bs_opera_mac: getOSXDesktop(BROWSER_OPERA, 46) // bs_chrome_mac_old: getOSXDesktop(BROWSER_CHROME, 16), <-- issues testing Push on old Chrome versions in BrowserStack // bs_firefox_mobile: getMobile(BROWSER_CHROME) <-- can't work because of HTTPS requirement (wth dude) }; ================================================ FILE: tests/karma.conf.js ================================================ // Karma configuration // Generated on Tue Jul 21 2015 22:34:30 GMT-0400 (EDT) let browsers, selected_browsers; browsers = require('./browsers.conf'); selected_browsers = Object.keys(browsers); const browser = process.env.PUSHJS_TEST_BROWSER; module.exports = function(config) { config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '../', ...(!browser && { browserStack: { username: 'Nickersoft', accessKey: 'peTScQRRBpSkOGjybGpd' } }), coverageReporter: { // specify a common output directory dir: 'coverage', reporters: [ { type: 'lcov', subdir: '.' } ] }, // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: ['jasmine'], plugins: [ 'karma-jasmine', 'karma-mocha-reporter', 'karma-coverage', 'karma-sourcemap-loader', ...(browser ? [`karma-${browser.toLowerCase()}-launcher`] : ['karma-browserstack-launcher']) ], // list of files / patterns to load in the browser files: [ './node_modules/platform/platform.js', './node_modules/@babel/polyfill/dist/polyfill.min.js', './bin/push.min.js', './tests/push.tests.js', './src/serviceWorker.min.js' ], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { './bin/push.js': ['sourcemap', 'coverage'] }, // src results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter reporters: ['mocha', 'coverage', ...(!browser ? ['BrowserStack'] : [])], // web server port port: 9876, // enable / disable colors in the output (reporters and logs) colors: true, // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: config.LOG_INFO, // enable / disable watching file and executing src whenever any file changes autoWatch: false, // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: browser ? [browser] : selected_browsers, // Continuous Integration mode // if true, Karma captures browsers, runs the src and exits singleRun: true, // custom browser launchers for BrowserStack ...(!browser && { customLaunchers: browsers }) }); }; ================================================ FILE: tests/push.tests.js ================================================ var BROWSER_CHROME = 'Chrome', BROWSER_FIREFOX = 'Firefox', BROWSER_EDGE = 'Edge', BROWSER_OPERA = 'Opera', TEST_TITLE = 'title', TEST_BODY = 'body', TEST_TIMEOUT = 1000, TEST_TAG = 'foo', TEST_TAG_2 = 'bar', TEST_ICON = 'icon', TEST_SW_DEFAULT = '/serviceWorker.min.js', NOOP = function() { return null; }; describe('proper support detection', function() { function isBrowser(browser) { return platform.name.toLowerCase() === browser.toLowerCase(); } function getVersion() { return parseFloat(platform.version); } function isSupported() { return Push.supported(); } it('should detect Firefox support correctly', function() { if (isBrowser(BROWSER_FIREFOX)) { if (getVersion() > 21) expect(isSupported()).toBeTruthy(); else expect(isSupported()).toBeFalsy(); } else { pending(); } }); it('should detect Chrome support correctly', function() { if (isBrowser(BROWSER_CHROME)) { if (getVersion() > 4) expect(isSupported()).toBeTruthy(); else expect(isSupported()).toBeFalsy(); } else { pending(); } }); it('should detect Opera support correctly', function() { if (isBrowser(BROWSER_OPERA)) { if (getVersion() > 23) expect(isSupported()).toBeTruthy(); else expect(isSupported()).toBeFalsy(); } else { pending(); } }); it('should detect Edge support correctly', function() { if (isBrowser(BROWSER_EDGE)) { if (getVersion() > 14) expect(isSupported()).toBeTruthy(); else expect(isSupported()).toBeFalsy(); } else { pending(); } }); }); describe('adding plugins', function() { it('reject invalid plugin manifests', function() { var testPlugin = function() { this.testFunc = function() {}; }; expect(Push.extend.bind(Push, testPlugin)).toThrow(); }); it('accept valid plugin manifests', function() { var testPlugin = { plugin: function() { this.testFunc = function() {}; } }; Push.extend(testPlugin); expect(Push.testFunc).toBeDefined(); }); it('only allow object-based configs', function() { spyOn(window.Push, 'config'); var testPlugin = { config: null, plugin: function() { this.testFunc = function() {}; } }; Push.extend(testPlugin); expect(Push.config.calls.count()).toEqual(1); // config() is called one by default in extend() var testPlugin2 = { config: {}, plugin: function() { this.testFunc = function() {}; } }; Push.extend(testPlugin2); expect(Push.config.calls.count()).toBeGreaterThan(1); }); }); describe('changing configuration', function() { it('returns the current configuration if no parameters passed', function() { var output = { serviceWorker: TEST_SW_DEFAULT, fallback: function(payload) {} }; var configKeys = Object.keys(Push.config()); Object.keys(output).forEach(function(k) { expect(configKeys.includes(k)).toBeTruthy(); }); }); it('adds a configuration if one is specified', function() { Push.config({ a: 1 }); expect(Push.config().a).toBeDefined(); expect(Push.config().a).toBe(1); }); it('should be capable of performing a deep merge', function() { var input1 = { b: { c: 1, d: { e: 2, f: 3 } } }; var input2 = { b: { d: { e: 2, f: 4 }, g: 5 } }; var output = { c: 1, d: { e: 2, f: 4 }, g: 5 }; Push.config(input1); Push.config(input2); expect(Push.config().b).toBeDefined(); expect(JSON.stringify(Push.config().b)).toBe(JSON.stringify(output)); }); }); if (Push.supported()) { Push.config({ serviceWorker: TEST_SW_DEFAULT }); function initRequestSpy(granted) { var param_str, param_int; param_str = granted ? Push.Permission.GRANTED : Push.Permission.DEFAULT; param_int = granted ? 0 : 1; /* Safari 6+, Legacy webkit browsers */ if ( window.webkitNotifications && window.webkitNotifications.checkPermission ) { spyOn(window.webkitNotifications, 'requestPermission').and.callFake( function(cb) { cb(param_int); } ); } else if ( /* Chrome 23+ */ window.Notification && window.Notification.requestPermission ) { spyOn(window.Notification, 'requestPermission').and.callFake( function() { return new Promise(function(resolve) { resolve(param_str); }); } ); } } function getRequestObject() { var obj = {}; /* Safari 6+, Legacy webkit browsers */ if ( window.webkitNotifications && window.webkitNotifications.checkPermission ) return window.webkitNotifications.requestPermission; /* Chrome 23+ */ else if ( window.Notification && window.Notification.requestPermission ) return window.Notification.requestPermission; return null; } describe('initialization', function() { it('should create a new instance', function() { expect(window.Push !== undefined).toBeTruthy(); }); it('isSupported should return a boolean', function() { expect(typeof Push.supported()).toBe('boolean'); }); }); describe('permission', function() { var callback; // Callback spy beforeAll(function() { jasmine.clock().uninstall(); jasmine.clock().install(); }); beforeEach(function() { callback = jasmine.createSpy('callback'); }); it('should have permission stored as a string varant', function() { expect(typeof Push.Permission.get()).toBe('string'); }); it('should update permission value if permission is denied and execute callback (deprecated)', function(done) { spyOn(Push.Permission, 'get').and.returnValue( Push.Permission.DEFAULT ); initRequestSpy(false); Push.Permission.request(NOOP, function() { callback(); expect(Push.Permission.has()).toBe(false); expect(callback).toHaveBeenCalled(); done(); }); }); it('should update permission value if permission is denied and execute callback (with promise)', function(done) { spyOn(Push.Permission, 'get').and.returnValue( Push.Permission.DEFAULT ); initRequestSpy(false); Push.Permission.request() .then(NOOP) .catch(function() { callback(); expect(callback).toHaveBeenCalled(); expect(Push.Permission.has()).toBe(false); done(); }); }); it('should request permission if permission is not granted', function() { spyOn(Push.Permission, 'get').and.returnValue( Push.Permission.DEFAULT ); initRequestSpy(false); Push.create(TEST_TITLE).then(function() { expect(getRequestObject()).toHaveBeenCalled(); }); }); it('should update permission value if permission is granted and execute callback (deprecated)', function() { spyOn(Push.Permission, 'get').and.returnValue( Push.Permission.GRANTED ); initRequestSpy(true); Push.Permission.request(callback, NOOP); jasmine.clock().tick(TEST_TIMEOUT); expect(Push.Permission.has()).toBe(true); expect(callback).toHaveBeenCalled(); }); it('should update permission value if permission is granted and execute callback (with promise)', function() { spyOn(Push.Permission, 'get').and.returnValue( Push.Permission.GRANTED ); initRequestSpy(true); Push.Permission.request(); jasmine.clock().tick(TEST_TIMEOUT); expect(Push.Permission.has()).toBe(true); }); it('should not request permission if permission is already granted', function() { spyOn(Push.Permission, 'get').and.returnValue( Push.Permission.GRANTED ); initRequestSpy(true); Push.Permission.request(); Push.create(TEST_TITLE) .then(function() { expect(getRequestObject()).not.toHaveBeenCalled(); }) .catch(function() {}); }); }); describe('creating notifications', function() { beforeAll(function() { jasmine.clock().uninstall(); jasmine.clock().install(); spyOn(Push.Permission, 'get').and.returnValue( Push.Permission.DEFAULT ); initRequestSpy(true); }); beforeEach(function() { Push.clear(); }); it('should throw exception if no title is provided', function() { expect(function() { Push.create(); }).toThrow(); }); it('should return a valid notification wrapper', function(done) { Push.create(TEST_TITLE).then(function(wrapper) { expect(wrapper).not.toBe(undefined); expect(wrapper.get).not.toBe(undefined); expect(wrapper.close).not.toBe(undefined); done(); }); }); it('should return promise successfully', function() { var promise = Push.create(TEST_TITLE) .then(function() { expect(promise.then).not.toBe(undefined); }) .catch(function() {}); }); it('should pass in all API options correctly', function(done) { // Vibrate omitted because Firefox will default to using the Notification API, not service workers // Timeout, requestPermission, and event listeners also omitted from this src :( Push.create(TEST_TITLE, { body: TEST_BODY, icon: TEST_ICON, tag: TEST_TAG, silent: true }).then(function(wrapper) { var notification = wrapper.get(); // Some browsers, like Safari, choose to omit this info if (notification.title) expect(notification.title).toBe(TEST_TITLE); if (notification.body) expect(notification.body).toBe(TEST_BODY); if (notification.icon) expect(notification.icon).toContain(TEST_ICON); // Some browsers append the document location, so we gotta use toContain() expect(notification.tag).toBe(TEST_TAG); if (notification.hasOwnProperty('silent')) { expect(notification.silent).toBe(true); } done(); }); }); it('should return the increase the notification count', function(done) { expect(Push.count()).toBe(0); Push.create(TEST_TITLE).then(function() { expect(Push.count()).toBe(1); done(); }); }); }); describe('event listeners', function() { var callback, // callback spy testListener = function(name, cb) { var event = new Event(name), options = {}, key, promise; key = 'on' + name[0].toUpperCase() + name.substr(1, name.length - 1); options[key] = callback; Push.create(TEST_TITLE, options) .then(function(wrapper) { var notification = wrapper.get(); notification.dispatchEvent(event); expect(callback).toHaveBeenCalled(); cb(); }) .catch(function() {}); }; beforeAll(function() { initRequestSpy(true); }); beforeEach(function() { callback = jasmine.createSpy('callback'); spyOn(Push.Permission, 'get').and.returnValue( Push.Permission.DEFAULT ); }); it('should execute onClick listener correctly', function(done) { testListener('click', done); }); it('should execute onShow listener correctly', function(done) { testListener('show', done); }); it('should execute onError listener correctly', function(done) { testListener('error', done); }); it('should execute onClose listener correctly', function(done) { testListener('close', done); }); }); describe('closing notifications', function() { var callback; // Callback spy var closeSpy; beforeAll(function() { initRequestSpy(true); jasmine.clock().uninstall(); jasmine.clock().install(); }); beforeEach(function() { closeSpy = spyOn(window.Notification.prototype, 'close'); spyOn(Push.Permission, 'get').and.returnValue( Push.Permission.DEFAULT ); closeSpy.calls.reset(); Push.clear(); callback = jasmine.createSpy('callback'); }); it('should close notifications on close callback', function(done) { Push.create(TEST_TITLE, { onClose: callback }).then(function(wrapper) { var notification = wrapper.get(); expect(Push.count()).toBe(1); notification.dispatchEvent(new Event('close')); expect(Push.count()).toBe(0); done(); }); }); it('should close notifications using wrapper', function(done) { Push.create(TEST_TITLE, { onClose: callback }).then(function(wrapper) { expect(Push.count()).toBe(1); wrapper.close(); expect(closeSpy).toHaveBeenCalled(); expect(Push.count()).toBe(0); done(); }); }); it('should close notifications using given timeout', function(done) { Push.create(TEST_TITLE, { timeout: TEST_TIMEOUT }).then(function() { jasmine.clock().tick(500); expect(Push.count()).toBe(1); expect(closeSpy).not.toHaveBeenCalled(); jasmine.clock().tick(TEST_TIMEOUT); expect(closeSpy).toHaveBeenCalled(); expect(Push.count()).toBe(0); done(); }); }); it('should close a notification given a tag', function(done) { Push.create(TEST_TITLE, { tag: TEST_TAG }).then(function() { expect(Push.count()).toBe(1); expect(Push.close(TEST_TAG)).toBeTruthy(); expect(closeSpy).toHaveBeenCalled(); expect(Push.count()).toBe(0); done(); }); }); it('should close all notifications when cleared', function(done) { Promise.all([ Push.create(TEST_TITLE, { tag: TEST_TAG }), Push.create('hello world!', { tag: TEST_TAG_2 }) ]).then(function() { expect(Push.count()).toBeGreaterThan(0); expect(Push.clear()).toBeTruthy(); expect(closeSpy).toHaveBeenCalled(); expect(Push.count()).toBe(0); done(); }); }); }); } else { describe('fallback functionality', function() { it('should ensure fallback method fires correctly', function(done) { var fallback = jasmine.createSpy('fallback'); Push.config({ fallback: fallback }); Push.create(TEST_TITLE).then(function() { expect(fallback).toHaveBeenCalled(); done(); }); }); it('should ensure all notification options are passed to the fallback', function(done) { Push.config({ fallback: function(payload) { expect(payload.title).toBe(TEST_TITLE); expect(payload.body).toBe(TEST_BODY); expect(payload.icon).toBe(TEST_ICON); expect(payload.tag).toBe(TEST_TAG); done(); } }); Push.create(TEST_TITLE, { body: TEST_BODY, icon: TEST_ICON, tag: TEST_TAG }).catch(function() {}); }); }); }