├── .babelrc ├── .gitignore ├── .npmrc ├── .travis.yml ├── LICENSE ├── NOTICE ├── README.md ├── RELEASING.md ├── examples ├── .nojekyll └── index.html ├── package.json ├── postcss.config.js ├── rollup.config.js ├── src ├── bundle.js ├── index.js ├── microfeedback-button.css └── send-json.js └── test ├── helpers └── setup-browser-env.js └── test-microfeedback-button.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "plugins": [ 11 | "external-helpers", 12 | "transform-object-assign" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | *.log 3 | # Built files 4 | es/* 5 | dist/* 6 | lib/* 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | sudo: false 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Steven Loria and Lauren Barker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | CSS for the feedback widget was adapted from ga-feedback. 2 | 3 | ga-feedback License 4 | =================== 5 | 6 | Copyright © 2015-2016 Xavi Esteve (http://xaviesteve.com) 7 | 8 | Permission is hereby granted, free of charge, to any person 9 | obtaining a copy of this software and associated documentation 10 | files (the “Software”), to deal in the Software without 11 | restriction, including without limitation the rights to use, 12 | copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the 14 | Software is furnished to do so, subject to the following 15 | conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | OTHER DEALINGS IN THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # microfeedback-button 2 | 3 | [![Current Version](https://img.shields.io/npm/v/microfeedback-button.svg)](https://www.npmjs.org/package/microfeedback-button) 4 | [![Build Status](https://travis-ci.org/microfeedback/microfeedback-button.svg?branch=master)](https://travis-ci.org/microfeedback/microfeedback-button) 5 | 6 | A simple widget for capturing user feedback. Use together with a microfeedback backend such as [microfeedback-github](https://github.com/microfeedback/microfeedback-github). 7 | 8 | Uses [sweetalert2](https://sweetalert2.github.io/) under the hood to 9 | display responsive, customizable, and accessible input dialogs. 10 | 11 | ## Documentation (with demos) 12 | 13 | https://microfeedback.js.org/ui-components/microfeedback-button/ 14 | 15 | ## API 16 | 17 | ### `microfeedback([elem], [options])` 18 | 19 | - `elem`: The `HTMLElement` to bind to. If not given, the default button 20 | will be rendered. 21 | - `options` 22 | - `url`: URL for your microfeedback backend. If `null`, 23 | feedback will be logged to the console. May also be a function that 24 | receives `btn` and `result` (the user input) as arguments and returns a URL. Default: `null` 25 | - `buttonText`: Text to display in the default button. Default: `'Feedback'` 26 | - `buttonAriaLabel`: `aria-label` for the default button. Default: `'Send feedback'` 27 | - `title`: Title to display in the dialog. Default: `'Send feedback'` 28 | - `placeholder`: Placeholder text in the dialog input. Default: `'Describe your issue or share your ideas'` 29 | - `backgroundColor`: Background color for the default button. Default: `'#3085d6'` 30 | - `color`: Color for the default button text. Default: `'#fff'` 31 | - `animation`: Enable animations. Default: `true` 32 | - `showDialog`: Function that displays a sweetalert2 dialog. Returns a 33 | `Promise` that resolves to the input result. Use `return btn.alert(...)` to 34 | display the dialog. 35 | - `getPayload`: Function that receives `btn` (the 36 | `MicroFeedbackButton` instance) and input result and returns 37 | the request payload to send to the microfeedback backend. 38 | - `preSend`: Function that receives `btn` (the 39 | `MicroFeedbackButton` instance) and input result. This is called 40 | before sending the request to the microfeedback backend. Useful for 41 | displaying a "Thank you" message with `return btn.alert(...)`. 42 | - `optimistic`: If `true`, display success message immediately after 43 | user submits input (don't wait for request to finish). If `false`, 44 | wait until request finishes to show message (use together with 45 | `onSuccess` to customize message). Default: `true` 46 | - `showSuccessDialog`: Function that receives `btn` (the 47 | `MicroFeedbackButton` instance) and input result and 48 | displays a dialog using `return btn.alert(...)`. 49 | - `onSuccess`: Function called when request succeeds. Receives `btn` (the 50 | `MicroFeedbackButton` instance) and input result. By default, 51 | calls `options.showSuccessDialog(btn, input)` if `optimistic` is 52 | `false`, otherwise noop. 53 | - `onFailure`: Function called when request fails. Receives `btn` (the 54 | `MicroFeedbackButton` instance). Default: `noop` 55 | 56 | Additionally, any valid [sweetalert2](https://sweetalert2.github.io/#configuration) option may be 57 | passed to configure the input dialog. 58 | 59 | 60 | ### Methods 61 | 62 | #### `btn.alert(...args)` 63 | 64 | Display a sweetalert2 dialog. This is equivalent to the `swal` function 65 | from sweetalert2. 66 | 67 | ## Developing 68 | 69 | * `npm install` 70 | * To run tests: `npm test` 71 | * To run tests in watch mode: `npm test -- --watch` 72 | * To run the example: `npm run dev` 73 | 74 | ## License 75 | 76 | MIT Licensed. 77 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | ``` 4 | npm run release 5 | ``` 6 | -------------------------------------------------------------------------------- /examples/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microfeedback/microfeedback-button/430228740cfd2b0b0366b831a055748e39637840/examples/.nojekyll -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | microfeedback-button Demo 7 | 36 | 37 | 38 | 39 | 40 | 41 |
42 |
43 |

microfeedback-button

44 |

microfeedback-button is a small library for adding a feedback button to your site.

45 |

It uses sweetalert2 for displaying beautiful, responsive input dialogs.

46 |

47 | Use this library together with a microfeedback backend, such as 48 | microfeedback-github, 49 | which will record user feedback as GitHub issues. 50 |

51 | 52 |

Default button

53 |

The default button with no configuration is at the bottom right of the page.

54 | 55 |

Binding to a specific element

56 |

You can bind the feedback dialog to any element.

57 | 58 | 59 | 60 |

Success message after response

61 |

62 | By default, the "thank you" message will be shown as soon as the user submits their 63 | input. Set optimistic: false if you want to show the message 64 | only after the request to the backend is complete. 65 |

66 | 67 | 68 | 69 |

Handling failure

70 |

71 | Use onFailure() to handle failed requests when optimistic 72 | is false. 73 |

74 | 75 | 76 | 77 |

Custom success dialog

78 |

Use the showSuccessDialog() hook to customize the success dialog.

79 | 80 | 81 |

showSuccessDialog() can be used together with optimistic: false.

82 | 83 | 84 | 85 |

Advanced: Multiple inputs

86 |

Here is an example using sweetalert2's preConfirm() function to support multiple inputs. 87 | See the sweetalert2 docs for more information. 88 |

89 | 90 | 91 |

Documentation

92 |

93 | View the project's documentation on GitHub: https://microfeedback.js.org/ui-components/microfeedback-button 94 |

95 |
96 |
97 | 98 | 99 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microfeedback-button", 3 | "version": "1.1.0", 4 | "description": "A simple feedback button/widget. Use together with a microfeedback backend.", 5 | "browser": "dist/microfeedback-button.js", 6 | "main": "lib/microfeedback-button.js", 7 | "module": "es/microfeedback-button.js", 8 | "scripts": { 9 | "dev": "NODE_ENV=development SERVE=true rollup -c --watch -o dist/microfeedback-button.js", 10 | "build:commonjs": "NODE_ENV=cjs rollup -c -o lib/microfeedback-button.js", 11 | "build:es": "NODE_ENV=es rollup -c -o es/microfeedback-button.js", 12 | "build:umd": "NODE_ENV=development rollup -c -o dist/microfeedback-button.js", 13 | "build:umd:min": "NODE_ENV=production rollup -c -o dist/microfeedback-button.min.js", 14 | "build": "npm-run-all --parallel build:commonjs build:es build:umd build:umd:min", 15 | "clean": "rimraf lib dist es", 16 | "prebuild": "npm run clean", 17 | "prepare": "npm run build", 18 | "pretest": "npm run build:umd", 19 | "lint": "xo src/* && stylelint src/*.css", 20 | "test": "npm run lint && NODE_ENV=test ava test --serial", 21 | "test:debug": "NODE_ENV=test iron-node ./node_modules/ava/profile.js test/tests.js", 22 | "release": "np" 23 | }, 24 | "files": [ 25 | "es", 26 | "lib", 27 | "dist", 28 | "src" 29 | ], 30 | "repository": "microfeedback/microfeedback-button", 31 | "authors": [ 32 | "Steven Loria (https://github.com/sloria)", 33 | "Lauren Barker (https://github.com/laurenbarker)" 34 | ], 35 | "license": "MIT", 36 | "keywords": [ 37 | "user", 38 | "customer", 39 | "feedback", 40 | "ui", 41 | "widget", 42 | "button", 43 | "microfeedback" 44 | ], 45 | "devDependencies": { 46 | "autoprefixer": "^9.0.0", 47 | "ava": "^3.0.0", 48 | "babel-cli": "^6.24.1", 49 | "babel-plugin-external-helpers": "^6.22.0", 50 | "babel-plugin-transform-object-assign": "^6.22.0", 51 | "babel-preset-env": "^1.6.1", 52 | "browser-env": "^3.2.0", 53 | "cssnano": "^4.0.0", 54 | "np": "^6.0.0", 55 | "npm-run-all": "^4.0.2", 56 | "rimraf": "^3.0.0", 57 | "rollup": "^1.29.1", 58 | "rollup-plugin-babel": "^3.0.3", 59 | "rollup-plugin-filesize": "^6.2.1", 60 | "rollup-plugin-json": "^4.0.0", 61 | "rollup-plugin-node-resolve": "^5.2.0", 62 | "rollup-plugin-postcss": "^2.0.4", 63 | "rollup-plugin-serve": "^1.0.1", 64 | "rollup-plugin-uglify": "^6.0.0", 65 | "rollup-watch": "^4.3.1", 66 | "sinon": "^9.0.0", 67 | "stylelint": "^13.0.0", 68 | "stylelint-config-standard": "^20.0.0", 69 | "syn": "^0.14.1", 70 | "xo": "^0.23.0" 71 | }, 72 | "dependencies": { 73 | "sweetalert2": "^7.22.2" 74 | }, 75 | "ava": { 76 | "require": [ 77 | "./test/helpers/setup-browser-env.js" 78 | ] 79 | }, 80 | "xo": { 81 | "envs": [ 82 | "node", 83 | "browser" 84 | ], 85 | "space": true, 86 | "rules": { 87 | "capitalized-comments": 0, 88 | "operator-linebreak": 0, 89 | "import/no-unassigned-import": 0, 90 | "no-warning-comments": 0, 91 | "comma-dangle": [ 92 | "error", 93 | "always-multiline" 94 | ] 95 | } 96 | }, 97 | "stylelint": { 98 | "extends": "stylelint-config-standard", 99 | "rules": { 100 | "declaration-colon-newline-after": null 101 | } 102 | }, 103 | "browserslist": [ 104 | "IE >= 11" 105 | ], 106 | "prettier": { 107 | "bracketSpacing": false, 108 | "useTabs": false, 109 | "singleQuote": true, 110 | "trailingComma": "all" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const cssnano = require('cssnano'); 2 | 3 | const PROD = process.env.NODE_ENV === 'production'; 4 | 5 | module.exports = { 6 | plugins: [ 7 | require('autoprefixer'), 8 | PROD && cssnano(), 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path'; 2 | 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | import json from 'rollup-plugin-json'; 5 | import serve from 'rollup-plugin-serve'; 6 | import {uglify} from 'rollup-plugin-uglify'; 7 | import babel from 'rollup-plugin-babel'; 8 | import postcss from 'rollup-plugin-postcss'; 9 | import filesize from 'rollup-plugin-filesize'; 10 | 11 | const env = process.env.NODE_ENV; 12 | 13 | const config = { 14 | plugins: [], 15 | }; 16 | 17 | if (env === 'cjs' || env === 'es') { 18 | config.input = resolve('src', 'index.js'); 19 | config.output = {format: env}; 20 | config.external = ['sweetalert2']; 21 | config.plugins.push(postcss(), babel()); 22 | } 23 | 24 | if (env === 'development' || env === 'production') { 25 | config.input = resolve('src', 'bundle.js'); 26 | config.output = { 27 | name: 'microfeedback', 28 | format: 'umd', 29 | globals: {sweetalert2: 'swal'}, 30 | }; 31 | config.plugins.push( 32 | postcss(), 33 | nodeResolve({ 34 | jsnext: true, 35 | }), 36 | babel({ 37 | exclude: ['**/*.json'], 38 | }), 39 | json() 40 | ); 41 | } 42 | 43 | if (env === 'production') { 44 | config.plugins.push(uglify()); 45 | } 46 | 47 | if (process.env.SERVE === 'true') { 48 | config.plugins.push(serve({contentBase: ['dist', 'examples'], open: true})); 49 | } 50 | 51 | config.plugins.push(filesize()); 52 | export default config; 53 | -------------------------------------------------------------------------------- /src/bundle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Entry point for UMD builds that bundles 3 | * sweetalert2's CSS. 4 | */ 5 | import microfeedback from '.'; 6 | import 'sweetalert2/dist/sweetalert2.css'; 7 | 8 | export default microfeedback; 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import swal from 'sweetalert2'; 2 | import './microfeedback-button.css'; 3 | import sendJSON from './send-json'; 4 | 5 | // Less typing 6 | const d = document; 7 | const noop = () => {}; 8 | const clickedClass = 'microfeedback-button--clicked'; 9 | 10 | const makeButton = options => 11 | ``; 16 | const defaults = { 17 | url: null, 18 | buttonText: 'Feedback', 19 | buttonAriaLabel: 'Send feedback', 20 | title: 'Send feedback', 21 | placeholder: 'Describe your issue or share your ideas', 22 | extra: null, 23 | backgroundColor: '#3085d6', 24 | color: '#fff', 25 | optimistic: true, 26 | showDialog: btn => { 27 | const swalOpts = { 28 | title: btn.options.title, 29 | input: 'textarea', 30 | inputPlaceholder: btn.options.placeholder, 31 | showCancelButton: true, 32 | confirmButtonText: 'Send', 33 | }; 34 | if (!btn.options.optimistic) { 35 | swalOpts.showLoaderOnConfirm = true; 36 | swalOpts.preConfirm = value => btn.onSubmit({value}); 37 | swalOpts.allowOutsideClick = () => !swal.isLoading(); 38 | } 39 | // Allow passing any valid sweetalert2 options 40 | Object.keys(btn.options).forEach(each => { 41 | if (swal.isValidParameter(each)) { 42 | swalOpts[each] = btn.options[each]; 43 | } 44 | }); 45 | return btn.alert(swalOpts); 46 | }, 47 | showSuccessDialog: btn => { 48 | return btn.alert( 49 | 'Thank you!', 50 | 'Your feedback has been submitted.', 51 | 'success' 52 | ); 53 | }, 54 | getPayload: (btn, {value: body}) => { 55 | const payload = {body}; 56 | if (btn.options.extra) { 57 | payload.extra = btn.options.extra; 58 | } 59 | return payload; 60 | }, 61 | preSend: (btn, input) => { 62 | if (btn.options.optimistic) { 63 | // Show thank you message before request is sent so the 64 | // user doesn't have to wait 65 | return btn.options.showSuccessDialog(btn, input); 66 | } 67 | }, 68 | sendRequest: (btn, url, payload) => { 69 | return sendJSON({ 70 | url, 71 | method: 'POST', 72 | payload, 73 | }); 74 | }, 75 | onSuccess: (btn, input, response) => { 76 | if (!btn.options.optimistic) { 77 | return btn.options.showSuccessDialog(btn, input, response); 78 | } 79 | }, 80 | onFailure: noop, 81 | }; 82 | 83 | class MicroFeedbackButton { 84 | constructor(element, options) { 85 | const opts = element instanceof HTMLElement ? options : element; 86 | this.options = Object.assign({}, defaults, opts); 87 | if (!this.options.url) { 88 | console.warn( 89 | 'options.url not provided. Feedback will only be logged to the console.' 90 | ); 91 | } 92 | 93 | this.appended = false; 94 | this._parent = null; 95 | if (element instanceof HTMLElement) { 96 | this.el = element; 97 | } else { 98 | // assume element is an object 99 | const buttonParent = d.createElement('div'); 100 | buttonParent.innerHTML = makeButton(this.options); 101 | d.body.appendChild(buttonParent); 102 | this._parent = buttonParent; 103 | this.appended = true; 104 | this.el = buttonParent.querySelector('.microfeedback-button'); 105 | } 106 | this.el.addEventListener('click', this.onClick.bind(this), false); 107 | } 108 | 109 | alert(...args) { 110 | return swal(...args); 111 | } 112 | 113 | onSubmit(input) { 114 | // Backend requires body in payload 115 | if (input.dismiss) { 116 | return null; 117 | } 118 | const payload = this.options.getPayload(this, input); 119 | // microfeedback backends requires 'body' 120 | if (payload.body) { 121 | this.options.preSend(this, input); 122 | let promise; 123 | const url = 124 | typeof this.options.url === 'function' 125 | ? this.options.url(this, input) 126 | : this.options.url; 127 | if (url) { 128 | promise = this.options.sendRequest(this, url, payload, input); 129 | } else { 130 | console.debug('microfeedback payload:'); 131 | console.debug(payload); 132 | promise = Promise.resolve(payload); 133 | } 134 | return promise.then( 135 | this.options.onSuccess.bind(this, this, input), 136 | this.options.onFailure.bind(this, this, input) 137 | ); 138 | } 139 | } 140 | 141 | onClick(e) { 142 | // eslint-disable-next-line no-unused-expressions 143 | e && e.preventDefault(); 144 | this.el.classList.add(clickedClass); 145 | const promise = this.options.showDialog(this).then(input => { 146 | this.el.classList.remove(clickedClass); 147 | return input; 148 | }); 149 | if (this.options.optimistic) { 150 | promise.then(this.onSubmit.bind(this)); 151 | } 152 | return promise; 153 | } 154 | 155 | destroy() { 156 | this.el.removeEventListener('click', this.onClick.bind(this)); 157 | if (this.appended) { 158 | d.body.removeChild(this._parent); 159 | } 160 | } 161 | } 162 | 163 | const factory = (element, options) => new MicroFeedbackButton(element, options); 164 | factory.MicroFeedbackButton = MicroFeedbackButton; 165 | export default factory; 166 | -------------------------------------------------------------------------------- /src/microfeedback-button.css: -------------------------------------------------------------------------------- 1 | button.microfeedback-button { 2 | cursor: pointer; 3 | text-decoration: none; 4 | position: fixed; 5 | bottom: 0; 6 | right: 3.125em; 7 | padding: 4px 7px; 8 | font-size: 0.875em; 9 | border-radius: 5px 5px 0 0; 10 | z-index: 1001; 11 | transition: all 0.2s ease-in-out; 12 | 13 | /* Button reset */ 14 | border: none; 15 | margin: 0; 16 | width: auto; 17 | overflow: visible; 18 | background: transparent; 19 | color: inherit; 20 | 21 | /* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */ 22 | line-height: normal; 23 | 24 | /* Corrects font smoothing for webkit */ 25 | -webkit-font-smoothing: inherit; 26 | -moz-osx-font-smoothing: inherit; 27 | 28 | /* Corrects inability to style clickable `input` types in iOS */ 29 | -webkit-appearance: none; 30 | } 31 | 32 | button.microfeedback-button:hover, 33 | button.microfeedback-button--clicked { 34 | /* Darken background color a bit */ 35 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNikAQAACIAHF/uBd8AAAAASUVORK5CYII="); 36 | padding-bottom: 10px; 37 | } 38 | -------------------------------------------------------------------------------- /src/send-json.js: -------------------------------------------------------------------------------- 1 | const defaults = { 2 | method: 'POST', 3 | url: null, 4 | payload: null, 5 | }; 6 | 7 | export default options => { 8 | const opts = Object.assign({}, defaults, options); 9 | return new Promise((resolve, reject) => { 10 | const req = new XMLHttpRequest(); 11 | req.open(opts.method, opts.url, true); 12 | req.setRequestHeader('Content-Type', 'application/json'); 13 | req.setRequestHeader('Accept', 'application/json'); 14 | req.send(JSON.stringify(opts.payload)); 15 | req.addEventListener('load', () => { 16 | if (req.status < 400) { 17 | const data = JSON.parse(req.response); 18 | resolve(data); 19 | } else { 20 | reject(new Error(req.statusText)); 21 | } 22 | }); 23 | req.addEventListener('error', () => { 24 | reject(new Error('Network Error')); 25 | }); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /test/helpers/setup-browser-env.js: -------------------------------------------------------------------------------- 1 | const browserEnv = require('browser-env'); 2 | 3 | browserEnv(); 4 | // Prevents an error when loading sweetalert2's weakmap polyfill 5 | window.WeakMap = () => {}; 6 | // Prevents warnings from being logged in tests 7 | window.scrollTo = () => {}; 8 | -------------------------------------------------------------------------------- /test/test-microfeedback-button.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import test from 'ava'; 3 | import syn from 'syn'; 4 | import {MicroFeedbackButton} from '../dist/microfeedback-button'; 5 | 6 | const $ = document.querySelector.bind(document); 7 | 8 | test('renders button', t => { 9 | const btn = new MicroFeedbackButton({url: false}); 10 | t.truthy($('.microfeedback-button')); 11 | btn.destroy(); 12 | }); 13 | 14 | test('optimistic mode is on by default', async t => { 15 | const onSuccessSpy = sinon.spy(); 16 | const btn = new MicroFeedbackButton({ 17 | onSuccess: onSuccessSpy, 18 | // simulate request 19 | url: true, 20 | sendRequest: () => Promise.resolve(), 21 | }); 22 | t.true(btn.options.optimistic); 23 | const alertSpy = sinon.spy(btn, 'alert'); 24 | await btn.onSubmit({value: 'foo'}); 25 | t.true(onSuccessSpy.called); 26 | t.true(alertSpy.called); 27 | t.deepEqual(onSuccessSpy.args[0][0], btn); 28 | t.deepEqual(onSuccessSpy.args[0][1], {value: 'foo'}); 29 | 30 | btn.alert.restore(); 31 | }); 32 | 33 | test('non-optimistic mode', async t => { 34 | const onSuccessSpy = sinon.spy(); 35 | const btn = new MicroFeedbackButton({ 36 | optimistic: false, 37 | onSuccess: onSuccessSpy, 38 | // simulate request 39 | url: true, 40 | sendRequest: () => Promise.resolve(), 41 | }); 42 | const alertSpy = sinon.spy(btn, 'alert'); 43 | await btn.onSubmit({value: 'foo'}); 44 | t.true(onSuccessSpy.called); 45 | // btn.alert should not be called when optimistic is false 46 | // because we show the thank you message is shown after the promise resolves 47 | t.false(alertSpy.called); 48 | t.deepEqual(onSuccessSpy.args[0][0], btn); 49 | t.deepEqual(onSuccessSpy.args[0][1], {value: 'foo'}); 50 | 51 | btn.alert.restore(); 52 | }); 53 | 54 | test('customizing the button text', t => { 55 | const btn = new MicroFeedbackButton({ 56 | url: false, 57 | buttonText: 'BeefDack', 58 | }); 59 | t.is(btn.el.innerHTML, 'BeefDack'); 60 | }); 61 | 62 | test.cb('clicking button shows dialog', t => { 63 | const btn = new MicroFeedbackButton({url: false}); 64 | syn.click(btn.el, () => { 65 | const popup = $('.swal2-popup'); 66 | t.truthy(popup); 67 | btn.destroy(); 68 | t.end(); 69 | }); 70 | }); 71 | 72 | test.cb('can type in dialog and submit', t => { 73 | const spy = sinon.spy(); 74 | const btn = new MicroFeedbackButton({url: false, animation: false, preSend: spy}); 75 | syn.click(btn.el).delay(() => { 76 | const input = $('.swal2-textarea'); 77 | syn.type(input, 'bar baz', () => { 78 | const submit = $('button.swal2-confirm'); 79 | syn.click(submit).delay(() => { 80 | t.truthy(spy.called); 81 | t.deepEqual(spy.args[0][1], {value: 'bar baz'}); 82 | btn.destroy(); 83 | t.end(); 84 | }); 85 | }, 200); 86 | }, 200); 87 | }); 88 | 89 | test.cb('sends request to URL', t => { 90 | const server = sinon.fakeServer.create(); 91 | const url = 'http://test.test/'; 92 | const btn = new MicroFeedbackButton({url}); 93 | const response = { 94 | backend: {name: 'github', version: '1.2.3'}, 95 | result: {}, 96 | }; 97 | server.respondWith('POST', url, [ 98 | 201, 99 | {'Content-Type': 'application/json'}, 100 | JSON.stringify(response), 101 | ]); 102 | syn.click(btn.el, () => { 103 | const input = $('.swal2-textarea'); 104 | syn.type(input, 'foo bar baz', () => { 105 | const submit = $('button.swal2-confirm'); 106 | syn.click(submit).delay(() => { 107 | server.respond(); 108 | t.is(server.requests.length, 1); 109 | btn.destroy(); 110 | t.end(); 111 | }); 112 | }); 113 | }); 114 | }); 115 | 116 | test.cb('sends request to URL returned by function', t => { 117 | const server = sinon.fakeServer.create(); 118 | const url = 'http://foo.test'; 119 | const btn = new MicroFeedbackButton({ 120 | url(btn, {value}) { 121 | t.true(value.includes('oo')); 122 | return url; 123 | }, 124 | }); 125 | const response = { 126 | backend: {name: 'github', version: '1.2.3'}, 127 | result: {}, 128 | }; 129 | server.respondWith('POST', url, [ 130 | 201, 131 | {'Content-Type': 'application/json'}, 132 | JSON.stringify(response), 133 | ]); 134 | syn.click(btn.el, () => { 135 | const input = $('.swal2-textarea'); 136 | syn.type(input, 'fool', () => { 137 | const submit = $('button.swal2-confirm'); 138 | syn.click(submit).delay(() => { 139 | server.respond(); 140 | t.is(server.requests.length, 1); 141 | t.is(server.requests[0].url, url); 142 | btn.destroy(); 143 | t.end(); 144 | }); 145 | }); 146 | }); 147 | }); 148 | 149 | test.cb('sends extra information in request', t => { 150 | const server = sinon.fakeServer.create(); 151 | const url = 'http://test.test/'; 152 | const btn = new MicroFeedbackButton({url, extra: {foo: 42}}); 153 | const response = { 154 | backend: {name: 'github', version: '1.2.3'}, 155 | result: {}, 156 | }; 157 | server.respondWith('POST', url, [ 158 | 201, 159 | {'Content-Type': 'application/json'}, 160 | JSON.stringify(response), 161 | ]); 162 | 163 | syn.click(btn.el, () => { 164 | const input = $('.swal2-textarea'); 165 | syn.type(input, 'foo bar baz', () => { 166 | const submit = $('button.swal2-confirm'); 167 | syn.click(submit).delay(() => { 168 | server.respond(); 169 | t.is(server.requests.length, 1); 170 | const reqBody = JSON.parse(server.requests[0].requestBody); 171 | t.deepEqual(reqBody.extra, {foo: 42}); 172 | btn.destroy(); 173 | t.end(); 174 | }); 175 | }); 176 | }); 177 | }); 178 | --------------------------------------------------------------------------------