├── .babelrc ├── .codebeatignore ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── Notifications_button_24.png ├── app.js ├── bundle.js ├── index.html ├── serviceWorker.html ├── sound.mp3 ├── sound.ogg └── sw.js ├── karma.conf.js ├── lib └── components │ └── Notification.js ├── package.json ├── src └── components │ └── Notification.js ├── test └── components │ └── Notification_spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env",{ 4 | "useBuiltIns": "entry", 5 | "corejs": 3 6 | } 7 | ], 8 | "@babel/react" 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /.codebeatignore: -------------------------------------------------------------------------------- 1 | lib/** 2 | example/** 3 | karma.conf.js 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.js] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [{package.json}] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | example/** 2 | node_modules/** 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true, 6 | "mocha": true 7 | }, 8 | "ecmaFeatures": { 9 | "jsx": true 10 | }, 11 | "parser": "babel-eslint", 12 | "plugins": [ 13 | "react" 14 | ], 15 | "rules": { 16 | "comma-spacing": 1, 17 | "key-spacing": 0, 18 | "no-underscore-dangle": 0, 19 | "no-unused-vars": [1, { "vars": "all", "args": "none" }], 20 | "no-var": 2, 21 | "quotes": [1, "single", "avoid-escape"], 22 | "react/display-name": 0, 23 | "react/jsx-no-undef": 1, 24 | "react/jsx-uses-react": 1, 25 | "react/no-did-mount-set-state": 1, 26 | "react/no-did-update-set-state": 1, 27 | "react/no-multi-comp": 1, 28 | "react/prop-types": [1, { "ignore": ["children", "className"] }], 29 | "react/react-in-jsx-scope": 1, 30 | "react/self-closing-comp": 1, 31 | "react/wrap-multilines": 1, 32 | "react/jsx-uses-vars": 1, 33 | "strict": 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | coverage 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | example -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | services: 5 | - xvfb 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Change Log 2 | 3 | ### Ver 0.8.0 4 | * Modern browsers support #66 5 | 6 | ### Ver 0.7.0 7 | * Enable to wait user interaction to close #60 8 | 9 | ### Ver 0.6.1 10 | * Update dependencies with `ncu -u` 11 | * Add codebeat 12 | 13 | ### Ver 0.6.0 14 | * Update dependencies, add core-js@3 15 | * Change url from georgeosddev to mobilusoss #48 16 | 17 | ### Ver 0.5.0 18 | 19 | * Update `Babel` from 6.x to 7.x 20 | * Update other dependencies 21 | 22 | ### Ver 0.4.0 23 | 24 | * [add service worker registration prop](https://github.com/mobilusoss/react-web-notification/pull/41) 25 | 26 | * [Add askAgain to propTypes in Readme](https://github.com/mobilusoss/react-web-notification/pull/42) 27 | 28 | ### Ver 0.3.2 29 | 30 | * [Typo issue](https://github.com/mobilusoss/react-web-notification/issues/39) 31 | 32 | ### Ver 0.3.1 33 | 34 | * [Change demo link to https](https://github.com/mobilusoss/react-web-notification/issues/33) 35 | 36 | ### Ver 0.3.0 37 | 38 | * [Update React and dependencies](https://github.com/mobilusoss/react-web-notification/issues/31) 39 | 40 | ### Ver 0.2.4 41 | 42 | * [#28 Use prop-types package instead of accessing PropTypes](https://github.com/mobilusoss/react-web-notification/issues/28), thanks @AlexNisnevich 43 | 44 | ### Ver 0.2.3 45 | 46 | * [#19 add support for react@^15](https://github.com/mobilusoss/react-web-notification/issues/19), thanks @emirotin 47 | 48 | ### Ver 0.2.2 49 | 50 | * [#15 Permission being repeatedly asked for in Safari](https://github.com/mobilusoss/react-web-notification/issues/11) 51 | 52 | ### Ver 0.2.1 53 | 54 | * [#11 depending on library](https://github.com/mobilusoss/react-web-notification/issues/11) 55 | 56 | ### Ver 0.2.0 57 | * [#7 Support React-v0.14](https://github.com/mobilusoss/react-web-notification/issues/7) 58 | * [#8 Publish transpiled code](https://github.com/mobilusoss/react-web-notification/issues/8) 59 | 60 | ### Ver 0.1.2 61 | * [#5 Add option `disableActiveWindow`](https://github.com/mobilusoss/react-web-notification/issues/5) 62 | 63 | ### Ver 0.1.1 64 | * [#2 Compile before publishing](https://github.com/mobilusoss/react-web-notification/issues/2) 65 | 66 | ### Ver 0.1.0 Initial release 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019 Mobilus Corporation 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-web-notification [![Build Status](https://travis-ci.org/mobilusoss/react-web-notification.svg?branch=develop)](https://travis-ci.org/mobilusoss/react-web-notification) [![npm version](https://badge.fury.io/js/react-web-notification.svg)](http://badge.fury.io/js/react-web-notification) [![codebeat badge](https://codebeat.co/badges/e03e06fa-8d28-44a9-afb9-848cfcf98d91)](https://codebeat.co/projects/github-com-mobilusoss-react-web-notification-master) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmobilusoss%2Freact-web-notification.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmobilusoss%2Freact-web-notification?ref=badge_shield) [![codecov](https://codecov.io/gh/mobilusoss/react-web-notification/branch/master/graph/badge.svg)](https://codecov.io/gh/mobilusoss/react-web-notification) 2 | 3 | React component with [HTML5 Web Notification API](https://developer.mozilla.org/en/docs/Web/API/notification). 4 | This component show nothing in dom element, but trigger HTML5 Web Notification API with `render` method in the life cycle of React.js. 5 | 6 | ## Demo 7 | 8 | [View Demo](https://mobilusoss.github.io/react-web-notification/example/) 9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm install --save react-web-notification 14 | ``` 15 | 16 | ## API 17 | 18 | ### `Notification` 19 | 20 | React component which wrap web-notification. 21 | 22 | #### Props 23 | 24 | ```javascript 25 | Notification.propTypes = { 26 | ignore: bool, 27 | disableActiveWindow: bool, 28 | askAgain: bool, 29 | notSupported: func, 30 | onPermissionGranted: func, 31 | onPermissionDenied: func, 32 | onShow: func, 33 | onClick: func, 34 | onClose: func, 35 | onError: func, 36 | timeout: number, 37 | title: string.isRequired, 38 | options: object, 39 | swRegistration: object, 40 | }; 41 | 42 | ``` 43 | 44 | * `ignore` : if true, nothing will be happen 45 | 46 | * `disableActiveWindow` : if true, nothing will be happen when window is active 47 | 48 | * `askAgain` : if true, `window.Notification.requestPermission` will be called on `componentDidMount`, even if it was denied before, 49 | 50 | * `notSupported()` : Called when [HTML5 Web Notification API](https://developer.mozilla.org/en/docs/Web/API/notification) is not supported. 51 | 52 | * `onPermissionGranted()` : Called when permission granted. 53 | 54 | * `onPermissionDenied()` : Called when permission denied. `Notification` will do nothing until permission granted again. 55 | 56 | * `onShow(e, tag)` : Called when Desktop notification is shown. 57 | 58 | * `onClick(e, tag)` : Called when Desktop notification is clicked. 59 | 60 | * `onClose(e, tag)` : Called when Desktop notification is closed. 61 | 62 | * `onError(e, tag)` : Called when Desktop notification happen error. 63 | 64 | * `timeout` : milli sec to close notification automatically. Ignored if `0` or less. (Default `5000`) 65 | 66 | * `title` : Notification title. 67 | 68 | * `options` : Notification options. set `body`, `tag`, `icon` here. 69 | See also (https://developer.mozilla.org/en-US/docs/Web/API/Notification/Notification) 70 | 71 | * `swRegistration` : ServiceWorkerRegistration. Use this prop to delegate the notification creation to a service worker. 72 | See also (https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification) 73 | ⚠️ `onShow`, `onClick`, `onClose` and `onError` handlers won't work when notification is created by the service worker. 74 | 75 | 76 | ## Usage example 77 | 78 | See [example](https://github.com/mobilusoss/react-web-notification/tree/develop/example) 79 | 80 | ```bash 81 | yarn 82 | yarn run start:example 83 | ``` 84 | 85 | ## Tests 86 | 87 | ```bash 88 | yarn test 89 | ``` 90 | 91 | ## Update dependencies 92 | 93 | Use [npm-check-updates](https://www.npmjs.com/package/npm-check-updates) 94 | 95 | ### Known Issues 96 | 97 | * [Notification.sound](https://github.com/mobilusoss/react-web-notification/issues/13) 98 | `Notification.sound` is [not supported in any browser](https://developer.mozilla.org/en/docs/Web/API/notification/sound#Browser_compatibility). 99 | You can emulate it with `onShow` callback. see [example](https://github.com/mobilusoss/react-web-notification/tree/develop/example). 100 | 101 | 102 | ## License 103 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmobilusoss%2Freact-web-notification.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmobilusoss%2Freact-web-notification?ref=badge_large) 104 | -------------------------------------------------------------------------------- /example/Notifications_button_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobilusoss/react-web-notification/295c444b77d7d601ed56030bf40211d9773d8269/example/Notifications_button_24.png -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import ReactDom from 'react-dom'; 5 | import Notification from '../lib/components/Notification'; 6 | 7 | //allow react dev tools work 8 | window.React = React; 9 | 10 | class App extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | ignore: true, 15 | title: '' 16 | }; 17 | } 18 | 19 | handlePermissionGranted(){ 20 | console.log('Permission Granted'); 21 | this.setState({ 22 | ignore: false 23 | }); 24 | } 25 | handlePermissionDenied(){ 26 | console.log('Permission Denied'); 27 | this.setState({ 28 | ignore: true 29 | }); 30 | } 31 | handleNotSupported(){ 32 | console.log('Web Notification not Supported'); 33 | this.setState({ 34 | ignore: true 35 | }); 36 | } 37 | 38 | handleNotificationOnClick(e, tag){ 39 | console.log(e, 'Notification clicked tag:' + tag); 40 | } 41 | 42 | handleNotificationOnError(e, tag){ 43 | console.log(e, 'Notification error tag:' + tag); 44 | } 45 | 46 | handleNotificationOnClose(e, tag){ 47 | console.log(e, 'Notification closed tag:' + tag); 48 | } 49 | 50 | handleNotificationOnShow(e, tag){ 51 | this.playSound(); 52 | console.log(e, 'Notification shown tag:' + tag); 53 | } 54 | 55 | playSound(filename){ 56 | document.getElementById('sound').play(); 57 | } 58 | 59 | handleButtonClick() { 60 | 61 | if(this.state.ignore) { 62 | return; 63 | } 64 | 65 | const now = Date.now(); 66 | 67 | const title = 'React-Web-Notification' + now; 68 | const body = 'Hello' + new Date(); 69 | const tag = now; 70 | const icon = 'http://mobilusoss.github.io/react-web-notification/example/Notifications_button_24.png'; 71 | // const icon = 'http://localhost:3000/Notifications_button_24.png'; 72 | 73 | // Available options 74 | // See https://developer.mozilla.org/en-US/docs/Web/API/Notification/Notification 75 | const options = { 76 | tag: tag, 77 | body: body, 78 | icon: icon, 79 | lang: 'en', 80 | dir: 'ltr', 81 | sound: './sound.mp3' // no browsers supported https://developer.mozilla.org/en/docs/Web/API/notification/sound#Browser_compatibility 82 | } 83 | this.setState({ 84 | title: title, 85 | options: options 86 | }); 87 | } 88 | 89 | handleButtonClick2() { 90 | this.props.swRegistration.getNotifications({}).then(function(notifications) { 91 | console.log(notifications); 92 | }); 93 | } 94 | 95 | render() { 96 | return ( 97 |
98 | 99 | {document.title === 'swExample' && } 100 | 114 | 119 |
120 | ) 121 | } 122 | }; 123 | if (document.title === 'swExample') { 124 | navigator.serviceWorker.register('sw.js') 125 | .then(function(registration) { 126 | ReactDom.render(, document.getElementById('out')); 127 | }); 128 | } else { 129 | ReactDom.render(, document.getElementById('out')); 130 | } 131 | 132 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React anchorify-text example 6 | 7 | 8 | 9 | Fork me on GitHub 10 |
11 | Icons made by Google from www.flaticon.com is licensed by CC BY 3.0 12 | 25 | 26 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /example/serviceWorker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | swExample 6 | 7 | 8 | 9 | Fork me on GitHub 10 |
11 | Icons made by Google from www.flaticon.com is licensed by CC BY 3.0 12 | 13 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /example/sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobilusoss/react-web-notification/295c444b77d7d601ed56030bf40211d9773d8269/example/sound.mp3 -------------------------------------------------------------------------------- /example/sound.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobilusoss/react-web-notification/295c444b77d7d601ed56030bf40211d9773d8269/example/sound.ogg -------------------------------------------------------------------------------- /example/sw.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobilusoss/react-web-notification/295c444b77d7d601ed56030bf40211d9773d8269/example/sw.js -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri May 15 2015 12:31:20 GMT+0900 (JST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['browserify','mocha'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'test/**/*_spec.js', 19 | { pattern: 'node_modules/sinon/pkg/sinon.js', watched: false, included: true } 20 | ], 21 | 22 | 23 | // list of files to exclude 24 | exclude: [ 25 | 'test/_helper/*.js' 26 | ], 27 | 28 | 29 | // preprocess matching files before serving them to the browser 30 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 31 | preprocessors: { 32 | 'test/**/*_spec.js': ['browserify'], 33 | 'src/**/*.js': ['coverage'] 34 | }, 35 | 36 | browserify: { 37 | debug: true, 38 | 'transform': [ 39 | [ 40 | 'babelify',{ 41 | "presets": [ 42 | ["@babel/env",{ 43 | "useBuiltIns": "entry", 44 | "corejs": 3 45 | } 46 | ], 47 | "@babel/react" 48 | ], 49 | "plugins": [ 50 | "istanbul", 51 | ], 52 | } 53 | ] 54 | ] 55 | }, 56 | 57 | // test results reporter to use 58 | // possible values: 'dots', 'progress' 59 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 60 | reporters: ['spec', 'coverage'], 61 | 62 | coverageReporter: { 63 | reporters: [ 64 | // reporters not supporting the `file` property 65 | {type:'lcovonly', subdir: '.'}, 66 | { type: 'text' }, 67 | ] 68 | }, 69 | 70 | // client: { 71 | // mocha: { 72 | // // reporter: 'html', // change Karma's debug.html to the mocha web reporter 73 | // // ui: 'tdd' 74 | // } 75 | // }, 76 | 77 | // web server port 78 | port: 9876, 79 | 80 | 81 | // enable / disable colors in the output (reporters and logs) 82 | colors: true, 83 | 84 | 85 | // level of logging 86 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 87 | logLevel: config.LOG_INFO, 88 | 89 | 90 | // enable / disable watching file and executing tests whenever any file changes 91 | autoWatch: true, 92 | 93 | 94 | // start these browsers 95 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 96 | // browsers: ['Chrome', 'Firefox', 'PhantomJS', 'IE'], 97 | browsers: [process.env.CONTINUOUS_INTEGRATION ? 'Firefox' : 'Chrome'], 98 | 99 | 100 | // Continuous Integration mode 101 | // if true, Karma captures browsers, runs the tests and exits 102 | singleRun: false 103 | }); 104 | }; 105 | -------------------------------------------------------------------------------- /lib/components/Notification.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _react = _interopRequireDefault(require("react")); 9 | 10 | var _propTypes = require("prop-types"); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 13 | 14 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 15 | 16 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 17 | 18 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 19 | 20 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 21 | 22 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 23 | 24 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 25 | 26 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 27 | 28 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 29 | 30 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 31 | 32 | var PERMISSION_GRANTED = 'granted'; 33 | var PERMISSION_DENIED = 'denied'; 34 | 35 | var seqGen = function seqGen() { 36 | var i = 0; 37 | return function () { 38 | return i++; 39 | }; 40 | }; 41 | 42 | var seq = seqGen(); // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API 43 | // https://github.com/mobilusoss/react-web-notification/issues/66 44 | 45 | function checkNotificationPromise() { 46 | try { 47 | window.Notification.requestPermission().then(); 48 | } catch (e) { 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | 55 | var Notification = 56 | /*#__PURE__*/ 57 | function (_React$Component) { 58 | _inherits(Notification, _React$Component); 59 | 60 | function Notification(props) { 61 | var _this; 62 | 63 | _classCallCheck(this, Notification); 64 | 65 | _this = _possibleConstructorReturn(this, _getPrototypeOf(Notification).call(this, props)); 66 | var supported = false; 67 | var granted = false; 68 | 69 | if ('Notification' in window && window.Notification) { 70 | supported = true; 71 | 72 | if (window.Notification.permission === PERMISSION_GRANTED) { 73 | granted = true; 74 | } 75 | } 76 | 77 | _this.state = { 78 | supported: supported, 79 | granted: granted 80 | }; // Do not save Notification instance in state 81 | 82 | _this.notifications = {}; 83 | _this.windowFocus = true; 84 | _this.onWindowFocus = _this._onWindowFocus.bind(_assertThisInitialized(_this)); 85 | _this.onWindowBlur = _this._onWindowBlur.bind(_assertThisInitialized(_this)); 86 | return _this; 87 | } 88 | 89 | _createClass(Notification, [{ 90 | key: "_onWindowFocus", 91 | value: function _onWindowFocus() { 92 | this.windowFocus = true; 93 | } 94 | }, { 95 | key: "_onWindowBlur", 96 | value: function _onWindowBlur() { 97 | this.windowFocus = false; 98 | } 99 | }, { 100 | key: "_askPermission", 101 | value: function _askPermission() { 102 | var _this2 = this; 103 | 104 | var handlePermission = function handlePermission(permission) { 105 | var result = permission === PERMISSION_GRANTED; 106 | 107 | _this2.setState({ 108 | granted: result 109 | }, function () { 110 | if (result) { 111 | _this2.props.onPermissionGranted(); 112 | } else { 113 | _this2.props.onPermissionDenied(); 114 | } 115 | }); 116 | }; 117 | 118 | if (checkNotificationPromise()) { 119 | window.Notification.requestPermission().then(function (permission) { 120 | handlePermission(permission); 121 | }); 122 | } else { 123 | window.Notification.requestPermission(function (permission) { 124 | handlePermission(permission); 125 | }); 126 | } 127 | } 128 | }, { 129 | key: "componentDidMount", 130 | value: function componentDidMount() { 131 | if (this.props.disableActiveWindow) { 132 | window.addEventListener('focus', this.onWindowFocus); 133 | window.addEventListener('blur', this.onWindowBlur); 134 | } 135 | 136 | if (!this.state.supported) { 137 | this.props.notSupported(); 138 | } else if (this.state.granted) { 139 | this.props.onPermissionGranted(); 140 | } else { 141 | if (window.Notification.permission === PERMISSION_DENIED) { 142 | if (this.props.askAgain) { 143 | this._askPermission(); 144 | } else { 145 | this.props.onPermissionDenied(); 146 | } 147 | } else { 148 | this._askPermission(); 149 | } 150 | } 151 | } 152 | }, { 153 | key: "componentWillUnmount", 154 | value: function componentWillUnmount() { 155 | if (this.props.disableActiveWindow) { 156 | window.removeEventListener('focus', this.onWindowFocus); 157 | window.removeEventListener('blur', this.onWindowBlur); 158 | } 159 | } 160 | }, { 161 | key: "doNotification", 162 | value: function doNotification() { 163 | var _this3 = this; 164 | 165 | var opt = this.props.options; 166 | 167 | if (typeof opt.tag !== 'string') { 168 | opt.tag = 'web-notification-' + seq(); 169 | } 170 | 171 | if (this.notifications[opt.tag]) { 172 | return; 173 | } 174 | 175 | if (this.props.swRegistration && this.props.swRegistration.showNotification) { 176 | this.props.swRegistration.showNotification(this.props.title, opt); 177 | this.notifications[opt.tag] = {}; 178 | } else { 179 | var n = new window.Notification(this.props.title, opt); 180 | 181 | n.onshow = function (e) { 182 | _this3.props.onShow(e, opt.tag); 183 | 184 | if (_this3.props.timeout > 0) { 185 | setTimeout(function () { 186 | _this3.close(n); 187 | }, _this3.props.timeout); 188 | } 189 | }; 190 | 191 | n.onclick = function (e) { 192 | _this3.props.onClick(e, opt.tag); 193 | }; 194 | 195 | n.onclose = function (e) { 196 | _this3.props.onClose(e, opt.tag); 197 | }; 198 | 199 | n.onerror = function (e) { 200 | _this3.props.onError(e, opt.tag); 201 | }; 202 | 203 | this.notifications[opt.tag] = n; 204 | } 205 | } 206 | }, { 207 | key: "render", 208 | value: function render() { 209 | var doNotShowOnActiveWindow = this.props.disableActiveWindow && this.windowFocus; 210 | 211 | if (!this.props.ignore && this.props.title && this.state.supported && this.state.granted && !doNotShowOnActiveWindow) { 212 | this.doNotification(); 213 | } // return null cause 214 | // Error: Invariant Violation: Notification.render(): A valid ReactComponent must be returned. You may have returned undefined, an array or some other invalid object. 215 | 216 | 217 | return _react["default"].createElement("input", { 218 | type: "hidden", 219 | name: "dummy-for-react-web-notification", 220 | style: { 221 | display: 'none' 222 | } 223 | }); 224 | } 225 | }, { 226 | key: "close", 227 | value: function close(n) { 228 | if (n && typeof n.close === 'function') { 229 | n.close(); 230 | } 231 | } // for debug 232 | 233 | }, { 234 | key: "_getNotificationInstance", 235 | value: function _getNotificationInstance(tag) { 236 | return this.notifications[tag]; 237 | } 238 | }]); 239 | 240 | return Notification; 241 | }(_react["default"].Component); 242 | 243 | Notification.propTypes = { 244 | ignore: _propTypes.bool, 245 | disableActiveWindow: _propTypes.bool, 246 | askAgain: _propTypes.bool, 247 | notSupported: _propTypes.func, 248 | onPermissionGranted: _propTypes.func, 249 | onPermissionDenied: _propTypes.func, 250 | onShow: _propTypes.func, 251 | onClick: _propTypes.func, 252 | onClose: _propTypes.func, 253 | onError: _propTypes.func, 254 | timeout: _propTypes.number, 255 | title: _propTypes.string.isRequired, 256 | options: _propTypes.object, 257 | swRegistration: _propTypes.object 258 | }; 259 | Notification.defaultProps = { 260 | ignore: false, 261 | disableActiveWindow: false, 262 | askAgain: false, 263 | notSupported: function notSupported() {}, 264 | onPermissionGranted: function onPermissionGranted() {}, 265 | onPermissionDenied: function onPermissionDenied() {}, 266 | onShow: function onShow() {}, 267 | onClick: function onClick() {}, 268 | onClose: function onClose() {}, 269 | onError: function onError() {}, 270 | timeout: 5000, 271 | options: {}, 272 | swRegistration: null 273 | }; 274 | var _default = Notification; 275 | exports["default"] = _default; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-web-notification", 3 | "version": "0.8.0", 4 | "description": "React component with HTML5 Web Notification API", 5 | "main": "./lib/components/Notification.js", 6 | "scripts": { 7 | "browser": "browser-sync start --files example/* --server example", 8 | "watch:example": "watchify example/app.js -dv -o example/bundle.js", 9 | "start:example": "yarn run watch:example & yarn run browser", 10 | "test:local": "karma start", 11 | "test": "NODE_EVN=test ./node_modules/.bin/karma start --browsers Firefox --single-run && codecov", 12 | "clean": "rimraf lib", 13 | "build": "babel src --out-dir lib" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/mobilusoss/react-web-notification" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "react-component", 22 | "notification", 23 | "web notification" 24 | ], 25 | "author": "Takeharu.Oshida", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/mobilusoss/react-web-notification/issues" 29 | }, 30 | "homepage": "https://github.com/mobilusoss/react-web-notification", 31 | "devDependencies": { 32 | "@babel/cli": "^7.5.0", 33 | "@babel/core": "^7.5.4", 34 | "@babel/preset-env": "^7.5.4", 35 | "@babel/preset-react": "^7.0.0", 36 | "babel-eslint": "^10.0.2", 37 | "babel-plugin-istanbul": "^5.2.0", 38 | "babelify": "^10.0.0", 39 | "browser-sync": "^2.26.7", 40 | "browserify": "^16.3.0", 41 | "chai": "^4.2.0", 42 | "codecov": "^3.5.0", 43 | "eslint": "^6.0.1", 44 | "eslint-plugin-react": "^7.14.2", 45 | "karma": "^4.2.0", 46 | "karma-browserify": "^6.1.0", 47 | "karma-chai": "^0.1.0", 48 | "karma-chrome-launcher": "^3.0.0", 49 | "karma-cli": "2.0.0", 50 | "karma-coverage": "^1.1.2", 51 | "karma-firefox-launcher": "^1.1.0", 52 | "karma-mocha": "^1.3.0", 53 | "karma-safari-launcher": "^1.0.0", 54 | "karma-spec-reporter": "0.0.32", 55 | "mocha": "^6.1.4", 56 | "prop-types": "^15.7.2", 57 | "react": "^16.8.6", 58 | "react-addons-test-utils": "^15.6.2", 59 | "react-dom": "^16.8.6", 60 | "rimraf": "^2.6.3", 61 | "sinon": "^7.3.2", 62 | "watchify": "^3.11.1" 63 | }, 64 | "peerDependencies": { 65 | "react": "^16.8.6" 66 | }, 67 | "browserify": { 68 | "transform": [ 69 | [ 70 | "babelify" 71 | ] 72 | ] 73 | }, 74 | "dependencies": { 75 | "core-js": "3" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/Notification.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { bool, func, number, object, string } from 'prop-types'; 3 | 4 | const PERMISSION_GRANTED = 'granted'; 5 | const PERMISSION_DENIED = 'denied'; 6 | 7 | const seqGen = () => { 8 | let i = 0; 9 | return () => { 10 | return i++; 11 | }; 12 | }; 13 | const seq = seqGen(); 14 | 15 | // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API 16 | // https://github.com/mobilusoss/react-web-notification/issues/66 17 | export const checkNotificationPromise = function() { 18 | try { 19 | window.Notification.requestPermission().then(); 20 | } catch(e) { 21 | return false; 22 | } 23 | return true; 24 | } 25 | class Notification extends React.Component { 26 | constructor(props) { 27 | super(props); 28 | 29 | let supported = false; 30 | let granted = false; 31 | if (('Notification' in window) && window.Notification) { 32 | supported = true; 33 | if (window.Notification.permission === PERMISSION_GRANTED) { 34 | granted = true; 35 | } 36 | } 37 | 38 | this.state = { 39 | supported: supported, 40 | granted: granted 41 | }; 42 | // Do not save Notification instance in state 43 | this.notifications = {}; 44 | this.windowFocus = true; 45 | this.onWindowFocus = this._onWindowFocus.bind(this); 46 | this.onWindowBlur = this._onWindowBlur.bind(this); 47 | } 48 | 49 | _onWindowFocus(){ 50 | this.windowFocus = true; 51 | } 52 | 53 | _onWindowBlur(){ 54 | this.windowFocus = false; 55 | } 56 | 57 | _askPermission(){ 58 | const handlePermission = (permission) => { 59 | let result = permission === PERMISSION_GRANTED; 60 | this.setState({ 61 | granted: result 62 | }, () => { 63 | if (result) { 64 | this.props.onPermissionGranted(); 65 | } else { 66 | this.props.onPermissionDenied(); 67 | } 68 | }); 69 | } 70 | if (checkNotificationPromise()) { 71 | window.Notification.requestPermission() 72 | .then((permission) => { 73 | handlePermission(permission); 74 | }) 75 | } else { 76 | window.Notification.requestPermission((permission) => { 77 | handlePermission(permission); 78 | }); 79 | } 80 | } 81 | 82 | componentDidMount(){ 83 | if (this.props.disableActiveWindow) { 84 | window.addEventListener('focus', this.onWindowFocus); 85 | window.addEventListener('blur', this.onWindowBlur); 86 | } 87 | 88 | if (!this.state.supported) { 89 | this.props.notSupported(); 90 | } else if (this.state.granted) { 91 | this.props.onPermissionGranted(); 92 | } else { 93 | if (window.Notification.permission === PERMISSION_DENIED){ 94 | if (this.props.askAgain){ 95 | this._askPermission(); 96 | } else { 97 | this.props.onPermissionDenied(); 98 | } 99 | } else { 100 | this._askPermission(); 101 | } 102 | } 103 | } 104 | 105 | componentWillUnmount(){ 106 | if (this.props.disableActiveWindow) { 107 | window.removeEventListener('focus', this.onWindowFocus); 108 | window.removeEventListener('blur', this.onWindowBlur); 109 | } 110 | } 111 | 112 | doNotification() { 113 | let opt = this.props.options; 114 | if (typeof opt.tag !== 'string') { 115 | opt.tag = 'web-notification-' + seq(); 116 | } 117 | if (this.notifications[opt.tag]) { 118 | return; 119 | } 120 | 121 | if (this.props.swRegistration && this.props.swRegistration.showNotification) { 122 | this.props.swRegistration.showNotification(this.props.title, opt) 123 | this.notifications[opt.tag] = {}; 124 | } else { 125 | const n = new window.Notification(this.props.title, opt); 126 | n.onshow = e => { 127 | this.props.onShow(e, opt.tag); 128 | if (this.props.timeout > 0) { 129 | setTimeout(() => { 130 | this.close(n); 131 | }, this.props.timeout); 132 | } 133 | }; 134 | n.onclick = e => { this.props.onClick(e, opt.tag); }; 135 | n.onclose = e => { this.props.onClose(e, opt.tag); }; 136 | n.onerror = e => { this.props.onError(e, opt.tag); }; 137 | 138 | this.notifications[opt.tag] = n; 139 | } 140 | } 141 | 142 | render() { 143 | let doNotShowOnActiveWindow = this.props.disableActiveWindow && this.windowFocus; 144 | if (!this.props.ignore && this.props.title && this.state.supported && this.state.granted && !doNotShowOnActiveWindow) { 145 | this.doNotification(); 146 | } 147 | 148 | // return null cause 149 | // Error: Invariant Violation: Notification.render(): A valid ReactComponent must be returned. You may have returned undefined, an array or some other invalid object. 150 | return ( 151 | 152 | ); 153 | } 154 | 155 | close(n) { 156 | if (n && typeof n.close === 'function') { 157 | n.close(); 158 | } 159 | } 160 | 161 | // for debug 162 | _getNotificationInstance(tag) { 163 | return this.notifications[tag]; 164 | } 165 | } 166 | 167 | Notification.propTypes = { 168 | ignore: bool, 169 | disableActiveWindow: bool, 170 | askAgain: bool, 171 | notSupported: func, 172 | onPermissionGranted: func, 173 | onPermissionDenied: func, 174 | onShow: func, 175 | onClick: func, 176 | onClose: func, 177 | onError: func, 178 | timeout: number, 179 | title: string.isRequired, 180 | options: object, 181 | swRegistration: object, 182 | }; 183 | 184 | Notification.defaultProps = { 185 | ignore: false, 186 | disableActiveWindow: false, 187 | askAgain: false, 188 | notSupported: () => {}, 189 | onPermissionGranted: () => {}, 190 | onPermissionDenied: () => {}, 191 | onShow: () => {}, 192 | onClick: () => {}, 193 | onClose: () => {}, 194 | onError: () => {}, 195 | timeout: 5000, 196 | options: {}, 197 | swRegistration: null, 198 | }; 199 | 200 | export default Notification; 201 | -------------------------------------------------------------------------------- /test/components/Notification_spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import ReactTestUtils from 'react-dom/test-utils'; 4 | 5 | import chai from 'chai'; 6 | import sinon from 'sinon'; 7 | let expect = chai.expect; 8 | import events from 'events' 9 | let EventEmitter = events.EventEmitter; 10 | import Notification, {checkNotificationPromise} from '../../src/components/Notification'; 11 | 12 | const PERMISSION_GRANTED = 'granted'; 13 | const PERMISSION_DENIED = 'denied'; 14 | const PERMISSION_DEFAULT = 'default'; 15 | 16 | describe('Test of Notification', () => { 17 | 18 | let component; 19 | 20 | describe('checkNotificationPromise', () => { 21 | describe('promise-based version of Notification.requestPermission() is supported', () => { 22 | let stub; 23 | before(() => { 24 | stub = sinon.stub(window.Notification, 'requestPermission').returns(Promise.resolve()); 25 | }) 26 | after(() => { 27 | stub.restore(); 28 | }) 29 | it('will return true', () => { 30 | expect(checkNotificationPromise()).to.be.eql(true); 31 | }) 32 | }); 33 | describe('when it does not support promise-based version', () => { 34 | let stub; 35 | before(() => { 36 | stub = sinon.stub(window.Notification, 'requestPermission').callsFake((cb) => {cb()}); 37 | }) 38 | after(() => { 39 | stub.restore(); 40 | }) 41 | it('will return false', () => { 42 | expect(checkNotificationPromise()).to.be.eql(false); 43 | }); 44 | }); 45 | }); 46 | describe('Notification component', () => { 47 | it('should have default properties', function () { 48 | component = ReactTestUtils.renderIntoDocument(); 49 | expect(component.props.ignore).to.be.eql(false); 50 | expect(component.props.disableActiveWindow).to.be.eql(false); 51 | expect(component.props.askAgain).to.be.eql(false); 52 | expect(typeof component.props.notSupported).to.be.eql('function'); 53 | expect(typeof component.props.onPermissionGranted).to.be.eql('function'); 54 | expect(typeof component.props.onPermissionDenied).to.be.eql('function'); 55 | expect(typeof component.props.onShow).to.be.eql('function'); 56 | expect(typeof component.props.onClick).to.be.eql('function'); 57 | expect(typeof component.props.onClose).to.be.eql('function'); 58 | expect(typeof component.props.onError).to.be.eql('function'); 59 | expect(component.props.timeout).to.be.eql(5000); 60 | expect(component.props.options).to.be.empty; 61 | }); 62 | 63 | it('should render dummy hidden tag', function () { 64 | component = ReactTestUtils.renderIntoDocument(); 65 | const el = ReactTestUtils.scryRenderedDOMComponentsWithTag(component, 'input'); 66 | expect(el.length).to.be.eql(1); 67 | expect(ReactDom.findDOMNode(el[0]).type).to.be.eql('hidden'); 68 | }); 69 | }); 70 | 71 | describe('Handling HTML5 Web Notification API', () => { 72 | describe('When Notification is not supported', () => { 73 | let cached, stub; 74 | 75 | before(() => { 76 | stub = sinon.stub(window.Notification, 'requestPermission'); 77 | cached = window.Notification; 78 | window.Notification = null; 79 | }); 80 | 81 | after(() => { 82 | window.Notification = cached; 83 | stub.restore(); 84 | }); 85 | 86 | it('should call notSupported prop', () => { 87 | let spy = sinon.spy(); 88 | component = ReactTestUtils.renderIntoDocument(); 89 | expect(spy.calledOnce).to.be.eql(true); 90 | expect(stub.called).to.be.eql(false); 91 | }); 92 | }); 93 | 94 | describe('When Notification is supported', () => { 95 | describe('start request permission ', () =>{ 96 | describe('When Notification is denied', () => { 97 | describe('Check callbacks', ()=> { 98 | 99 | let stub, spy1, spy2; 100 | before(() => { 101 | spy1 = sinon.spy(); 102 | spy2 = sinon.spy(); 103 | stub = sinon.stub(window.Notification, 'requestPermission').callsFake(function(cb){ 104 | if (typeof cb === 'function') cb(PERMISSION_DENIED); 105 | }); 106 | component = ReactTestUtils.renderIntoDocument(); 107 | }); 108 | 109 | after(() => { 110 | stub.restore(); 111 | }); 112 | 113 | it('should call window.Notification.requestPermission twice', () => { 114 | expect(stub.calledTwice).to.be.eql(true); 115 | expect(stub.getCall(0).args).to.be.eql([]); 116 | expect(stub.getCall(1).args.length).to.be.eql(1); 117 | expect(stub.getCall(1).args[0]).to.be.a('function'); 118 | }); 119 | 120 | it('should call onPermissionDenied prop', () => { 121 | expect(spy1.called).to.be.eql(false); 122 | expect(spy2.calledOnce).to.be.eql(true); 123 | }); 124 | }); 125 | }); 126 | 127 | describe('When Notification is already denied', () => { 128 | describe('Check callbacks', ()=> { 129 | 130 | let stub1, stub2, spy1, spy2; 131 | before(() => { 132 | spy1 = sinon.spy(); 133 | spy2 = sinon.spy(); 134 | stub1 = sinon.stub(window.Notification, 'permission').get(function getterFn() { 135 | return PERMISSION_DENIED; 136 | }); 137 | stub2 = sinon.stub(window.Notification, 'requestPermission').callsFake(function(cb){ 138 | return cb(PERMISSION_DENIED); 139 | }); 140 | component = ReactTestUtils.renderIntoDocument(); 141 | }); 142 | 143 | after(() => { 144 | stub1.restore(); 145 | stub2.restore(); 146 | }); 147 | 148 | it('should not call window.Notification.requestPermission', () => { 149 | expect(stub2.called).to.be.eql(false); 150 | }); 151 | 152 | it('should call onPermissionDenied prop', () => { 153 | expect(spy1.called).to.be.eql(false); 154 | expect(spy2.calledOnce).to.be.eql(true); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('When Notification is already denied, but `askAgain` prop is true', () => { 160 | describe('Check callbacks', ()=> { 161 | 162 | let stub1, stub2, spy1, spy2, spy3; 163 | before(() => { 164 | spy1 = sinon.spy(); 165 | spy2 = sinon.spy(); 166 | spy3 = sinon.spy(); 167 | stub1 = sinon.stub(window.Notification, 'permission').get(function getterFn() { 168 | return PERMISSION_DENIED; 169 | }); 170 | stub2 = sinon.stub(window.Notification, 'requestPermission').callsFake(function(cb){ 171 | return cb(PERMISSION_GRANTED); 172 | }); 173 | component = ReactTestUtils.renderIntoDocument(); 174 | }); 175 | 176 | after(() => { 177 | stub1.restore(); 178 | stub2.restore(); 179 | }); 180 | 181 | it('should call window.Notification.requestPermission', () => { 182 | it('should call window.Notification.requestPermission twice', () => { 183 | expect(stub.calledTwice).to.be.eql(true); 184 | expect(stub.getCall(0).args).to.be.eql([]); 185 | expect(stub.getCall(1).args.length).to.be.eql(1); 186 | expect(stub.getCall(1).args[0]).to.be.a('function'); 187 | }); 188 | }); 189 | 190 | it('should call onPermissionGranted prop', () => { 191 | expect(spy1.called).to.be.eql(false); 192 | expect(spy2.called).to.be.eql(false); 193 | expect(spy3.calledOnce).to.be.eql(true); 194 | }); 195 | }); 196 | }); 197 | 198 | describe('When Notification is granted', () => { 199 | let stub; 200 | before(() => { 201 | stub = sinon.stub(window.Notification, 'requestPermission').callsFake(function(cb){ 202 | return cb(PERMISSION_GRANTED); 203 | }); 204 | }); 205 | 206 | after(() => { 207 | stub.restore(); 208 | }); 209 | 210 | describe('Check callbacks', ()=> { 211 | let spy1, spy2, spy3; 212 | before(() => { 213 | spy1 = sinon.spy(); 214 | spy2 = sinon.spy(); 215 | spy3 = sinon.spy(); 216 | component = ReactTestUtils.renderIntoDocument(); 217 | }); 218 | 219 | it('should call window.Notification.requestPermission', () => { 220 | expect(stub.called).to.be.eql(true); 221 | }); 222 | 223 | it('should call onPermissionDenied prop', () => { 224 | expect(spy1.called).to.be.eql(false); 225 | expect(spy2.called).to.be.eql(false); 226 | expect(spy3.calledOnce).to.be.eql(true); 227 | }); 228 | }); 229 | 230 | describe('Handle component properties', () => { 231 | 232 | let stubConstructor, onShowSpy, onClickSpy, onCloseSpy, onErrorSpy, ee; 233 | 234 | beforeEach(() => { 235 | EventEmitter.prototype.addEventListener = EventEmitter.prototype.addListener; 236 | ee = new EventEmitter(); 237 | 238 | stubConstructor = sinon.stub(window, 'Notification'); 239 | stubConstructor.onFirstCall().returns(ee); 240 | }); 241 | 242 | afterEach(()=>{ 243 | // stub.restore(); 244 | stubConstructor.restore(); 245 | }); 246 | 247 | describe('when ignore prop is true', () => { 248 | onShowSpy = sinon.spy(); 249 | onClickSpy = sinon.spy(); 250 | onCloseSpy = sinon.spy(); 251 | onErrorSpy = sinon.spy(); 252 | 253 | it('does not trigger Notification', () => { 254 | component = ReactTestUtils.renderIntoDocument(); 255 | expect(stubConstructor.calledWithNew()).to.be.eql(false); 256 | expect(onShowSpy.called).to.be.eql(false); 257 | expect(onClickSpy.called).to.be.eql(false); 258 | expect(onCloseSpy.called).to.be.eql(false); 259 | expect(onErrorSpy.called).to.be.eql(false); 260 | }); 261 | }); 262 | 263 | describe('when ignore prop is false', () => { 264 | const MY_TITLE = 'mytitle'; 265 | const MY_OPTIONS = { 266 | tag: 'mytag', 267 | body: 'mybody', 268 | icon: 'myicon', 269 | lang: 'en', 270 | dir: 'ltr' 271 | }; 272 | onShowSpy = sinon.spy(); 273 | onClickSpy = sinon.spy(); 274 | onCloseSpy = sinon.spy(); 275 | onErrorSpy = sinon.spy(); 276 | 277 | it('trigger Notification with specified title and options', () => { 278 | component = ReactTestUtils.renderIntoDocument(); 279 | expect(stubConstructor.calledWithNew()).to.be.eql(true); 280 | expect(stubConstructor.calledWith(MY_TITLE, MY_OPTIONS)).to.be.eql(true); 281 | }); 282 | 283 | it('call onShow prop when notification is shown', () => { 284 | let n = component._getNotificationInstance('mytag'); 285 | n.onshow('showEvent'); 286 | expect(onShowSpy.calledOnce).to.be.eql(true); 287 | let args = onShowSpy.args[0]; 288 | expect(args[0]).to.be.eql('showEvent'); 289 | expect(args[1]).to.be.eql('mytag'); 290 | expect(onClickSpy.called).to.be.eql(false); 291 | expect(onCloseSpy.called).to.be.eql(false); 292 | expect(onErrorSpy.called).to.be.eql(false); 293 | }); 294 | 295 | it('call onClick prop when notification is clicked', () => { 296 | let n = component._getNotificationInstance('mytag'); 297 | n.onclick('clickEvent'); 298 | expect(onClickSpy.calledOnce).to.be.eql(true); 299 | let args = onClickSpy.args[0]; 300 | expect(args[0]).to.be.eql('clickEvent'); 301 | expect(args[1]).to.be.eql('mytag'); 302 | }); 303 | 304 | it('call onCLose prop when notification is closed', () => { 305 | let n = component._getNotificationInstance('mytag'); 306 | n.onclose('closeEvent'); 307 | expect(onCloseSpy.calledOnce).to.be.eql(true); 308 | let args = onCloseSpy.args[0]; 309 | expect(args[0]).to.be.eql('closeEvent'); 310 | expect(args[1]).to.be.eql('mytag'); 311 | }); 312 | 313 | it('call onError prop when notification throw error', () => { 314 | let n = component._getNotificationInstance('mytag'); 315 | n.onerror('errorEvent'); 316 | expect(onErrorSpy.calledOnce).to.be.eql(true); 317 | let args = onErrorSpy.args[0]; 318 | expect(args[0]).to.be.eql('errorEvent'); 319 | expect(args[1]).to.be.eql('mytag'); 320 | }); 321 | }); 322 | describe('test of autoClose timer', () => { 323 | const MY_TITLE = 'mytitle'; 324 | const MY_OPTIONS = { 325 | tag: 'mytag', 326 | body: 'mybody', 327 | icon: 'myicon', 328 | lang: 'en', 329 | dir: 'ltr' 330 | }; 331 | describe('when `props.timeout` is less than eql 0', () => { 332 | let n; 333 | before(() => { 334 | component = ReactTestUtils.renderIntoDocument(); 335 | n = component._getNotificationInstance('mytag'); 336 | sinon.stub(n, 'close'); 337 | n.onshow('showEvent'); 338 | }); 339 | after(() => { 340 | n.close.restore(); 341 | }) 342 | it('will not trigger close automatically', (done) => { 343 | setTimeout(() => { 344 | expect(n.close.called).to.be.eql(false); 345 | done(); 346 | }, 200); 347 | }) 348 | }) 349 | describe('when `props.timeout` is greater than 0', () => { 350 | let n; 351 | before(() => { 352 | component = ReactTestUtils.renderIntoDocument(); 353 | n = component._getNotificationInstance('mytag'); 354 | sinon.stub(n, 'close'); 355 | n.onshow('showEvent'); 356 | }); 357 | after(() => { 358 | n.close.restore(); 359 | }); 360 | it('will trigger close automatically', (done) => { 361 | setTimeout(() => { 362 | expect(n.close.called).to.be.eql(true); 363 | done(); 364 | }, 200); 365 | }) 366 | }) 367 | }) 368 | describe('when swRegistration prop is defined', () => { 369 | const swRegistrationMock = { showNotification: sinon.stub().resolves({ notification: ee }) } 370 | const MY_TITLE = 'mytitle'; 371 | const MY_OPTIONS = { 372 | tag: 'mytag', 373 | body: 'mybody', 374 | icon: 'myicon', 375 | lang: 'en', 376 | dir: 'ltr', 377 | requireInteraction: true, 378 | }; 379 | 380 | it('does not trigger Notification but trigger swRegistration.showNotification', () => { 381 | component = ReactTestUtils.renderIntoDocument(); 382 | expect(stubConstructor.calledWithNew()).to.be.eql(false); 383 | expect(swRegistrationMock.showNotification.calledWith(MY_TITLE, MY_OPTIONS)).to.be.eql(true); 384 | }); 385 | }); 386 | }); 387 | }); 388 | }); 389 | }); 390 | }); 391 | }); 392 | --------------------------------------------------------------------------------